Merge branch 'main' into feat-messaging

This commit is contained in:
Steven Nguyen
2024-01-17 23:08:24 +00:00
96 changed files with 8705 additions and 677 deletions
+6
View File
@@ -3,8 +3,14 @@ name: Tests
on:
push:
branches: [main]
paths-ignore:
- '**/*.md'
- 'static/**/*'
pull_request:
branches: [main]
paths-ignore:
- '**/*.md'
- 'static/**/*'
env:
VITE_APPWRITE_ENDPOINT: http://appwrite.test/v1
+4 -4
View File
@@ -6,7 +6,7 @@
"": {
"name": "@appwrite/console",
"dependencies": {
"@appwrite.io/console": "^0.4.2",
"@appwrite.io/console": "^0.5.0",
"@appwrite.io/pink": "0.2.0",
"@appwrite.io/pink-icons": "0.2.0",
"@popperjs/core": "^2.11.8",
@@ -159,9 +159,9 @@
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
},
"node_modules/@appwrite.io/console": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.4.2.tgz",
"integrity": "sha512-WUltkC5q7RIK7TDCb+qTXq2cPtBek9k98Ilqcn8Lv3rN5ZuK0x33AwcpTAEPD0DE9phWXmvoV1a8H5PGMcGKew==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.5.0.tgz",
"integrity": "sha512-WqjVc6lrmOwNGoyAGNx7z/sBJou1EgGbnGn87TuBvfhZ/zcYSdi+Nc010EwGpyUvPeNhL2ci8Jj3eYnelJVKwg==",
"dependencies": {
"cross-fetch": "3.1.5",
"isomorphic-form-data": "2.0.0"
+1 -1
View File
@@ -18,7 +18,7 @@
"e2e": "playwright test tests/e2e"
},
"dependencies": {
"@appwrite.io/console": "^0.4.2",
"@appwrite.io/console": "^0.5.0",
"@appwrite.io/pink": "0.2.0",
"@appwrite.io/pink-icons": "0.2.0",
"@popperjs/core": "^2.11.8",
+7647
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -139,6 +139,7 @@ export function isTrackingAllowed() {
}
export enum Submit {
DownloadDPA = 'submit_download_dpa',
Error = 'submit_error',
AccountCreate = 'submit_account_create',
AccountLogin = 'submit_account_login',
-1
View File
@@ -15,7 +15,6 @@
const { input, handleSubmit, completion, isLoading, complete, error } = useCompletion({
api: endpoint + '/console/assistant',
headers: {
'content-type': 'application/json',
'x-appwrite-project': 'console'
},
credentials: 'include'
@@ -1,11 +1,13 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { failedInvoice } from '$lib/stores/billing';
</script>
{#if $failedInvoice}
<HeaderAlert title="You are limited to one free organization per account">
All but one organization will be automatically upgraded to a Pro plan on <b>01 Feb 2024</b>.
You can add a payment method or transfer projects from your settings.
</HeaderAlert>
{/if}
<HeaderAlert title="You are limited to one free organization per account">
All but one organization will be automatically upgraded to a Pro plan on <b>31 January 2024</b>.
You can add a payment method or transfer projects from your settings.
<svelte:fragment slot="buttons">
<Button secondary href="/console/account/organizations">View organizations</Button>
</svelte:fragment>
</HeaderAlert>
@@ -2,6 +2,7 @@
import { FormList, InputText } from '$lib/elements/forms';
import { onDestroy, onMount } from 'svelte';
import { CreditCardBrandImage, RadioBoxes } from '..';
import { unmountPaymentElement } from '$lib/stores/stripe';
export let methods: Record<string, unknown>[];
export let group: string;
@@ -33,6 +34,7 @@
onDestroy(() => {
observer.disconnect();
unmountPaymentElement();
});
$: if (element) {
+1 -1
View File
@@ -10,7 +10,7 @@
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<slot />
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16">
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
<slot name="aside" />
</div>
</div>
+1 -1
View File
@@ -9,7 +9,7 @@
<details class="collapsible-wrapper" {open}>
<!-- svelte-ignore a11y-no-redundant-roles -->
<summary
class="collapsible-button"
class="collapsible-button u-position-relative"
on:keyup={clickOnEnter}
on:click
role="button"
+149
View File
@@ -0,0 +1,149 @@
<script lang="ts" context="module">
type Consent = {
key: string;
accepted: Record<string, boolean>;
};
export const settings = writable<boolean>(false);
export const show = writable<boolean>(false);
export const consent = writable<Consent>(
JSON.parse(globalThis?.localStorage?.getItem('consent') ?? null)
);
consent.subscribe((value) => {
if (browser) {
globalThis.localStorage.setItem('consent', JSON.stringify(value));
}
});
</script>
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { Modal } from '.';
import { Button } from '$lib/elements/forms';
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
const key = new Date('2023-11-07');
const dispatch = createEventDispatcher();
let selected = {};
$: if ($settings) {
selected = $consent?.accepted ?? {};
}
onMount(() => {
if ($consent) {
const date = new Date($consent.key);
if (key > date) {
show.set(true);
}
} else {
show.set(true);
}
});
function saveSettings(obj: Consent) {
consent.set(obj);
}
function confirmChoices(choices: Consent['accepted']) {
const consent = {
key: key.toISOString(),
accepted: choices
};
saveSettings(consent);
dispatch('confirm', consent);
show.set(false);
settings.set(false);
}
function acceptAll() {
confirmChoices({
analytics: true
});
}
function rejectAll() {
confirmChoices({});
}
</script>
{#if $show}
<div class="card is-consent">
<p>
By clicking "Accept all", you agree to the storing of cookies on your device to analyze
site usage.
</p>
<div
class="is-consent-buttons u-flex u-margin-block-start-16 u-main-space-between u-cross-center">
<Button class="u-padding-inline-0" text on:click={() => settings.set(true)}>
Cookie settings
</Button>
<div class="u-flex u-gap-16">
<Button secondary on:click={rejectAll}>Only required</Button>
<Button secondary on:click={acceptAll}>Accept all</Button>
</div>
</div>
</div>
{/if}
<Modal bind:show={$settings} title="Cookie Preferences">
<p>
We use cookies to improve your site experience. The "strictly necessary" cookies are
required for Appwrite to function.
</p>
<div class="u-flex-vertical u-gap-24 u-width-full-line" style:margin-block-end="24px">
<div class="u-flex u-gap-8">
<input type="checkbox" checked disabled />
<div>
<span class="text u-bold">Strictly necessary cookies</span>
<p class="text u-margin-block-start-8">
These are the cookies required for Appwrite to function.
</p>
</div>
</div>
<div class="u-flex u-gap-8">
<input id="analytics" type="checkbox" bind:checked={selected['analytics']} />
<div>
<label for="analytics" class="text u-bold">Product analytics</label>
<span class="">(optional)</span>
<p class="text u-margin-block-start-8">
We include analytics cookies to understand how you use our product and design
better experiences.
</p>
</div>
</div>
</div>
<svelte:fragment slot="footer">
<Button text external href="https://appwrite.io/privacy">Privacy Policy</Button>
<Button on:click={() => confirmChoices(selected)}>Save preferences</Button>
</svelte:fragment>
</Modal>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
.card {
position: fixed;
padding: 1.5rem;
bottom: 1rem;
right: 1rem;
z-index: 100;
max-width: 600px;
}
@media #{$break1} {
.card {
bottom: 0.5rem;
left: 0.5rem;
right: 0.5rem;
max-width: 100%;
.is-consent-buttons {
flex-direction: column;
align-items: center;
}
}
}
</style>
+2 -12
View File
@@ -67,11 +67,11 @@
on:click={handleBLur}
bind:this={backdrop}>
<div
class="modal payment-modal"
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-separate-header={headerDivider}>
<Form isModal {onSubmit} class="payment-form">
<Form isModal {onSubmit}>
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<div class="u-flex u-cross-center u-gap-16">
@@ -151,14 +151,4 @@
background-color: hsl(240 5% 8% / 0.6);
}
}
.payment-modal {
height: 100%;
overflow: hidden;
}
.payment-form {
height: 100%;
overflow: auto;
}
</style>
+2 -2
View File
@@ -115,7 +115,7 @@
<div>
<form on:submit|preventDefault={addFilter}>
<div class="selects u-flex u-gap-8 u-margin-block-start-16">
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
<InputSelect
id="column"
options={$columns
@@ -132,7 +132,7 @@
options={operatorsForColumn}
placeholder="Select operator"
bind:value={operatorKey} />
</div>
</ul>
{#if column && operator && !operator?.hideInput}
<div class="u-margin-block-start-8">
{#if column.type === 'integer' || column.type === 'double'}
+3 -3
View File
@@ -26,13 +26,13 @@
}
:global(.theme-dark) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-200));
background: hsl(var(--color-neutral-300));
border: 1px solid hsl(var(--color-neutral-85));
background: hsl(var(--color-neutral-90));
box-shadow: 0px 6px 16px 8px #14141f;
}
:global(.theme-light) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-30));
border: 1px solid hsl(var(--color-neutral-15));
background: hsl(var(--color-neutral-0));
box-shadow: 0px 6px 16px 0px rgba(55, 59, 77, 0.14);
}
+5 -2
View File
@@ -10,10 +10,13 @@
import { isSupportOnline, showSupportModal } from '../../routes/console/wizard/support/store';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
export let show = false;
$: isPaid = $organization?.billingPlan === 'tier-1' || $organization?.billingPlan === 'tier-2';
$: isPaid =
$organization?.billingPlan === BillingPlan.PRO ||
$organization?.billingPlan === BillingPlan.SCALE;
</script>
{#if isCloud}
@@ -39,7 +42,7 @@
</p>
{/if}
</div>
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button fullWidth href="https://appwrite.io/pricing" external>
<span class="text">Get Premium support</span>
</Button>
+7
View File
@@ -8,6 +8,7 @@ export enum Dependencies {
ADDRESS = 'dependency:address',
PAYMENT_METHODS = 'dependency:paymentMethods',
ORGANIZATION = 'dependency:organization',
MEMBERS = 'dependency:members',
PROJECT = 'dependency:project',
PROJECT_VARIABLES = 'dependency:project_variables',
PROJECT_INSTALLATIONS = 'dependency:project_installations',
@@ -622,3 +623,9 @@ export const limitRates = {
}
]
};
export enum BillingPlan {
STARTER = 'tier-0',
PRO = 'tier-1',
SCALE = 'tier-2'
}
+9 -1
View File
@@ -23,6 +23,7 @@
let classes: string = undefined;
export { classes as class };
export let actions: MultiActionArray = [];
export let submissionLoader = false;
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
@@ -59,6 +60,7 @@
{#if href}
<a
on:click
on:click={track}
{href}
target={external ? '_blank' : ''}
@@ -77,6 +79,12 @@
aria-label={ariaLabel}
type={submit ? 'submit' : 'button'}
use:multiAction={actions}>
<slot />
{#if $isSubmitting && submissionLoader}
<span
class="loader is-small"
style:--p-loader-base-full-color="transparent"
aria-hidden="true" />
{/if}
<slot isSubmitting={$isSubmitting} />
</button>
{/if}
+8 -2
View File
@@ -7,7 +7,7 @@
export let showLabel = true;
export let optionalText: string | undefined = undefined;
export let id: string;
export let value = '';
export let value: string;
export let required = false;
export let nullable = false;
export let disabled = false;
@@ -35,6 +35,11 @@
error = element.validationMessage;
}
function handleInput(event: Event) {
const { value: currentValue } = event.currentTarget as HTMLInputElement;
value = currentValue || null;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
@@ -64,12 +69,13 @@
{disabled}
{readonly}
{required}
{value}
step=".001"
autocomplete={autocomplete ? 'on' : 'off'}
type="datetime-local"
class="input-text"
bind:value
bind:this={element}
on:input={handleInput}
on:invalid={handleInvalid}
style:--amount-of-buttons={isNullable ? 2.75 : 1}
style:--button-size={isNullable ? '2rem' : '1rem'} />
+1 -1
View File
@@ -8,7 +8,7 @@
let element: HTMLInputElement;
let icon = 'info';
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`;
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._\-]*$`;
onMount(() => {
if (element && autofocus) {
+6 -2
View File
@@ -21,6 +21,8 @@
import { tierToPlan, type PlanServices } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
export let logs: Models.LogList;
export let offset = 0;
@@ -50,8 +52,10 @@
Logs are retained in rolling {hoursToDays(limit)} intervals with the
{tierToPlan($organization.billingPlan).name}
plan.
<button class="link" type="button" on:click|preventDefault={upgradeMethod}
>Upgrade</button> to increase your log retention for a longer period.
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button link on:click={upgradeMethod}>Upgrade</Button> to increase your log
retention for a longer period.
{/if}
</p>
</svelte:fragment>
{#each logs.logs as log}
+2 -1
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
export let title: string;
export let tooltipContent =
$organization.billingPlan === 'tier-0'
$organization.billingPlan === BillingPlan.STARTER
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization.billingPlan).name
+21 -21
View File
@@ -1,22 +1,23 @@
<script lang="ts">
import {
tierToPlan,
getServiceLimit,
type PlanServices,
showUsageRatesModal,
checkForUsageFees,
readOnly,
checkForProjectLimitation
} from '$lib/stores/billing';
import { Alert, DropList, Heading } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Pill } from '$lib/elements';
import { organization } from '$lib/stores/organization';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { createEventDispatcher, onMount } from 'svelte';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { ContainerButton } from '.';
import { Button } from '$lib/elements/forms';
import {
checkForProjectLimitation,
checkForUsageFees,
getServiceLimit,
readOnly,
showUsageRatesModal,
tierToPlan,
type PlanServices
} from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { ContainerButton } from '.';
export let isFlex = true;
export let title: string;
@@ -35,8 +36,7 @@
let showDropdown = false;
// TODO: remove the default billing limits when backend is updated with billing code
const { bandwidth, documents, storage, users, executions } = ($organization ?? {})
.billingLimits ?? {
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {
bandwidth: 1,
documents: 1,
storage: 1,
@@ -60,7 +60,7 @@
$: tier = tierToPlan($organization?.billingPlan)?.name;
$: hasProjectLimitation =
checkForProjectLimitation(serviceId) && $organization?.billingPlan === 'tier-0';
checkForProjectLimitation(serviceId) && $organization?.billingPlan === BillingPlan.STARTER;
$: hasUsageFees = hasProjectLimitation
? checkForUsageFees($organization?.billingPlan, serviceId)
: false;
@@ -85,7 +85,7 @@
})
.join(', ')}
<slot name="alert" {limit} {tier} {title} {upgradeMethod} {hasUsageFees} {services}>
{#if $organization?.billingPlan !== 'tier-0' && hasUsageFees}
{#if $organization?.billingPlan !== BillingPlan.STARTER && hasUsageFees}
<Alert type="info" isStandalone>
<span class="text">
You've reached the {services} limit for the {tier} plan.
@@ -129,7 +129,7 @@
<p class="text">
Your are limited to {limit}
{title.toLocaleLowerCase()} per project on the {tier} plan.
{#if $organization?.billingPlan === 'tier-0'}<Button
{#if $organization?.billingPlan === BillingPlan.STARTER}<Button
link
on:click={upgradeMethod}>Upgrade</Button>
for addtional {title.toLocaleLowerCase()}.
@@ -147,7 +147,7 @@
<p class="text">
You are limited to {limit}
{title.toLocaleLowerCase()} per organization on the {tier} plan.
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button link on:click={upgradeMethod}>Upgrade</Button>
for additional {title.toLocaleLowerCase()}.
{/if}
+14
View File
@@ -1,4 +1,6 @@
<script>
import { settings } from '$lib/components/consent.svelte';
import { clickOnEnter } from '$lib/helpers/a11y';
import { isCloud } from '$lib/system';
import { version } from '$routes/console/store';
@@ -39,6 +41,18 @@
<span class="text">Privacy</span>
</a>
</li>
{#if isCloud}
<li class="inline-links-item">
<span
style:cursor="pointer"
role="button"
tabindex="0"
on:keyup={clickOnEnter}
on:click={() => settings.set(true)}>
<span class="text">Cookies</span>
</span>
</li>
{/if}
</ul>
</div>
<div class="main-footer-end">
+2 -2
View File
@@ -27,6 +27,7 @@
import { Pill } from '$lib/elements';
import { showExcess } from '$routes/console/organization-[organization]/store';
import { readOnly } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
let showDropdown = false;
let showSupport = false;
@@ -117,10 +118,9 @@
<div class="main-header-end">
<nav class="u-flex is-only-desktop u-cross-center">
{#if isCloud && $organization?.billingPlan === 'tier-0' && !$page.url.pathname.startsWith('/console/account')}
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
<Button
disabled={$organization?.markedForDeletion}
secondary
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
Upgrade
</Button>
+10 -7
View File
@@ -19,6 +19,8 @@
import { getServiceLimit, tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { app } from '$lib/stores/app';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
@@ -310,13 +312,14 @@
<Alert>
Logs are retained in rolling {hoursToDays(limit)} intervals
with the {tier} plan.
<button
class="link"
type="button"
on:click|preventDefault={() =>
wizard.start(ChangeOrganizationTierCloud)}
>Upgrade</button> to increase your log retention for
a longer period.
{#if $organization.billingPlan === BillingPlan.STARTER}
<Button
link
on:click={() =>
wizard.start(ChangeOrganizationTierCloud)}
>Upgrade</Button> to increase your log retention
for a longer period.
{/if}
</Alert>
{/if}
<Code withCopy noMargin code={execution.logs} language="sh" />
+2 -1
View File
@@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { isMac } from '$lib/helpers/platform';
import { slide } from '$lib/helpers/transition';
import { organization } from '$lib/stores/organization';
@@ -197,7 +198,7 @@
</a>
<ul class="drop-list is-only-mobile">
{#if isCloud && $organization?.billingPlan !== 'tier-2'}
{#if isCloud && $organization?.billingPlan !== BillingPlan.SCALE}
<li class="drop-list-item">
<button
class="drop-button"
+1
View File
@@ -122,6 +122,7 @@
} else {
$wizard.step--;
}
wizard.setInterceptor(null);
trackEvent('wizard_back');
}
+8 -1
View File
@@ -8,10 +8,17 @@
function handleSubmit() {
dispatch('exit');
show = false;
}
</script>
<Modal title="Exit Process" bind:show onSubmit={handleSubmit} icon="exclamation" state="warning">
<Modal
title="Exit Process"
bind:show
onSubmit={handleSubmit}
icon="exclamation"
state="warning"
headerDivider={false}>
<p>
Are you sure you want to exit from <slot />? All data will be deleted. This action is
irreversible.
+19 -13
View File
@@ -185,15 +185,15 @@ export type AggregationList = {
};
export type AllowedRegions =
| 'eu-de'
| 'us-nyc'
| 'us-sfo'
| 'ap-in'
| 'eu-gb'
| 'eu-nl'
| 'ap-sg'
| 'ap-ca'
| 'ap-au'
| 'fra'
| 'nyc'
| 'sfo'
| 'blr'
| 'lon'
| 'ams'
| 'sgp'
| 'tor'
| 'syd'
| 'default'; //TODO: remove after migration
export type Region = {
@@ -265,6 +265,8 @@ export type PlansInfo = {
total: number;
};
export type PlansMap = Map<Tier, Plan>;
export class Billing {
client: Client;
@@ -276,14 +278,16 @@ export class Billing {
organizationId: string,
name: string,
billingPlan: string,
paymentMethodId: string
paymentMethodId: string,
billingAddressId: string
): Promise<Organization> {
const path = `/organizations`;
const params = {
organizationId,
name,
billingPlan,
paymentMethodId
paymentMethodId,
billingAddressId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
@@ -331,13 +335,15 @@ export class Billing {
async updatePlan(
organizationId: string,
billingPlan: string,
paymentMethodId: string
paymentMethodId: string,
billingAddressId: string
): Promise<Organization> {
const path = `/organizations/${organizationId}/plan`;
const params = {
organizationId,
billingPlan,
paymentMethodId
paymentMethodId,
billingAddressId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
+29 -34
View File
@@ -2,35 +2,35 @@ import { page } from '$app/stores';
import { derived, get, writable } from 'svelte/store';
import { sdk } from './sdk';
import { organization, type Organization } from './organization';
import type { InvoiceList, AddressesList, Invoice, PaymentList, PlansInfo } from '$lib/sdk/billing';
import type { InvoiceList, AddressesList, Invoice, PaymentList, PlansMap } from '$lib/sdk/billing';
import { isCloud } from '$lib/system';
import { cachedStore } from '$lib/helpers/cache';
import { Query, type Models } from '@appwrite.io/console';
import { headerAlert } from './headerAlert';
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
import { diffDays, toLocaleDate } from '$lib/helpers/date';
import { addNotification, notifications } from './notifications';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import TooManyFreOrgs from '$lib/components/billing/alerts/tooManyFreeOrgs.svelte';
import { activeHeaderAlert, showPostReleaseModal } from '$routes/console/store';
import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte';
import { BillingPlan } from '$lib/constants';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2';
export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList);
export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList);
export const plansInfo = derived(page, ($page) => $page.data.plansInfo as PlansInfo);
export const plansInfo = derived(page, ($page) => $page.data.plansInfo as PlansMap);
export const daysLeftInTrial = writable<number>(0);
export const readOnly = writable<boolean>(false);
export function tierToPlan(tier: Tier) {
switch (tier) {
case 'tier-0':
case BillingPlan.STARTER:
return tierFree;
case 'tier-1':
case BillingPlan.PRO:
return tierPro;
case 'tier-2':
case BillingPlan.SCALE:
return tierScale;
default:
return tierFree;
@@ -63,8 +63,8 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null): num
if (!isCloud) return 0;
if (!serviceId) return 0;
const info = get(plansInfo);
if (!info?.plans) return 0;
const plan = info.plans.find((p) => p.$id === (tier ?? get(organization)?.billingPlan));
if (!info) return 0;
const plan = info.get(tier ?? get(organization)?.billingPlan);
return plan?.[serviceId];
}
@@ -117,7 +117,7 @@ export const tierScale: TierData = {
export const showUsageRatesModal = writable<boolean>(false);
export function checkForUsageFees(plan: Tier, id: PlanServices) {
if (plan === 'tier-1' || plan === 'tier-2') {
if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) {
switch (id) {
case 'bandwidth':
case 'storage':
@@ -157,51 +157,44 @@ export function isServiceLimited(serviceId: PlanServices, plan: Tier, total: num
}
export function calculateTrialDay(org: Organization) {
if (org?.billingPlan === 'tier-0') return false;
const endDate = new Date(org?.billingTrialEndDate);
if (org?.billingPlan === BillingPlan.STARTER) return false;
const endDate = new Date(org?.billingStartDate);
const today = new Date();
const days = diffDays(today, endDate);
let diffTime = endDate.getTime() - today.getTime();
diffTime = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
const days = diffTime < 1 ? 0 : diffTime;
daysLeftInTrial.set(days);
return days;
}
export function checkForTrialEnding(org: Organization) {
const days = calculateTrialDay(org);
if (localStorage.getItem('trialEndingNotification') === 'true' || !days) return;
else if (days <= 5) {
addNotification({
type: 'info',
isHtml: true,
message: `<b>We hope you've been enjoying the ${
tierToPlan(org.billingPlan).name
} plan.</b>
You will be billed on a recurring 30-day cycle after your trial period ends on <b>${toLocaleDate(
org.billingTrialEndDate
)}</b>`
});
localStorage.setItem('trialEndingNotification', 'true');
}
}
export function checkForUsageLimit(org: Organization) {
export async function checkForUsageLimit(org: Organization) {
if (!org?.billingLimits) {
readOnly.set(false);
return;
}
const { bandwidth, documents, executions, storage, users } = org.billingLimits;
const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {};
const members = await sdk.forConsole.teams.listMemberships(org.$id);
const plan = get(plansInfo)?.get(org.billingPlan);
const membersOverflow =
members?.total > plan.members ? members.total - (plan.members || members.total) : 0;
if (
bandwidth >= 100 ||
documents >= 100 ||
executions >= 100 ||
storage >= 100 ||
users >= 100
users >= 100 ||
membersOverflow > 0
) {
readOnly.set(true);
} else readOnly.set(false);
}
export async function checkPaymentAuthorizationRequired(org: Organization) {
if (org.billingPlan === 'tier-0') return;
if (org.billingPlan === BillingPlan.STARTER) return;
const invoices = await sdk.forConsole.billing.listInvoices(org.$id, [
Query.equal('status', 'requires_authentication')
@@ -282,11 +275,13 @@ export async function checkForFreeOrgOverflow(orgs: Models.TeamList<Record<strin
show: true,
importance: 10
});
activeHeaderAlert.set(headerAlert.get());
}
}
export async function checkForPostReleaseProModal(orgs: Models.TeamList<Record<string, unknown>>) {
if (!orgs?.teams?.length) return;
if (orgs.total > orgs.teams.length) return; // if the total is greater that the free orgs it means that there are pro orgs
const modalTime = localStorage.getItem('postReleaseProModal');
const now = Date.now();
// show the modal if it was never shown
+1 -1
View File
@@ -14,7 +14,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
billingCurrentInvoiceDate: string;
billingNextInvoiceDate: string;
billingTrialStartDate?: string;
billingTrialEndDate?: string;
billingStartDate?: string;
billingTrialDays?: number;
billingAddressId?: string;
amount: number;
+10 -4
View File
@@ -37,7 +37,13 @@ export async function initializeStripe() {
paymentElement.mount('#payment-element');
}
// TODO: fix redirect
export async function unmountPaymentElement() {
isStripeInitialized.set(false);
paymentElement?.unmount();
clientSecret = null;
paymentMethod = null;
elements = null;
}
export async function submitStripeCard(name: string, urlRoute?: string) {
try {
@@ -71,7 +77,7 @@ export async function submitStripeCard(name: string, urlRoute?: string) {
if (error) {
const e = new Error(error.message);
trackError(e, Submit.PaymentMethodCreate);
throw error;
throw e;
}
if (setupIntent && setupIntent.status === 'succeeded') {
@@ -87,7 +93,7 @@ export async function submitStripeCard(name: string, urlRoute?: string) {
} else {
const e = new Error('Something went wrong');
trackError(e, Submit.PaymentMethodCreate);
throw e;
throw e.message;
}
} catch (e) {
trackError(e, Submit.PaymentMethodCreate);
@@ -109,7 +115,7 @@ export async function confirmPayment(orgId: string, clientSecret: string, paymen
}
});
if (error) {
throw new Error();
throw error.message;
}
} catch (e) {
addNotification({
+6 -2
View File
@@ -31,14 +31,18 @@ function createWizardStore() {
return {
subscribe,
set,
start: (component: typeof SvelteComponent<unknown>, media: string = null) =>
start: (
component: typeof SvelteComponent<unknown>,
media: string = null,
step: number = 1
) =>
update((n) => {
n.show = true;
n.component = component;
n.interceptor = null;
n.interceptorNotificationEnabled = true;
n.media = media;
n.step = 1;
n.step = step;
n.cover = null;
n.nextDisabled = false;
n.finalAction = null;
@@ -87,13 +87,13 @@
]}
value={null} />
</div>
<div class="u-width-full-line">
<ul class="u-width-full-line">
<InputSearch placeholder="Search repositories" disabled />
</div>
</ul>
</div>
{:then installations}
<div class="u-flex u-gap-16">
<div class="u-width-full-line">
<ul class="u-width-full-line">
<InputSelect
id="installation"
label="Select installation"
@@ -111,10 +111,10 @@
);
}}
bind:value={selectedInstallation} />
</div>
<div class="u-width-full-line">
</ul>
<ul class="u-width-full-line">
<InputSearch placeholder="Search repositories" bind:value={search} />
</div>
</ul>
</div>
{/await}
<p class="text u-margin-block-start-16">
@@ -63,7 +63,7 @@
{/await}
</FormList>
<div class="u-margin-block-start-24">
<FormList class="u-margin-block-start-24">
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
@@ -78,5 +78,5 @@
bind:id={$templateConfig.$id}
fullWidth />
{/if}
</div>
</FormList>
</WizardStep>
+5 -1
View File
@@ -16,6 +16,7 @@
import Loading from './loading.svelte';
import { loading, requestedMigration } from './store';
import { parseIfString } from '$lib/helpers/object';
import Consent, { consent } from '$lib/components/consent.svelte';
if (browser) {
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
@@ -51,7 +52,7 @@
/**
* LogRocket
*/
if (isCloud && isTrackingAllowed()) {
if ($consent?.accepted?.analytics && isCloud && isTrackingAllowed()) {
LogRocket.init('rgthvf/appwrite', {
dom: {
inputSanitizer: true
@@ -124,6 +125,9 @@
</script>
<Notifications />
{#if isCloud}
<Consent />
{/if}
<slot />
@@ -14,13 +14,15 @@
$: postText = encodeURIComponent(
[
`Appwrite Pro is now available! `,
`@appwrite Pro just launched!`,
``,
`Why did I upgrade?`,
`I am one of the first to join 🔥`,
``,
`Because`,
`Learn more about Appwrite Pro here: https://apwr.dev/AppwritePro`,
``,
`Discover Appwrite Pro and get started at https://appwrite.io/pricing`
`Ps. Limited edition Appwrite Pro zipper hoodies are up for grabs.`,
``,
`#AppwritePro`
].join('\n')
);
+3 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { INTERVAL } from '$lib/constants';
import { BillingPlan, INTERVAL } from '$lib/constants';
import { Logs } from '$lib/layout';
import Footer from '$lib/layout/footer.svelte';
import Header from '$lib/layout/header.svelte';
@@ -18,7 +18,6 @@
checkForUsageLimit,
checkPaymentAuthorizationRequired,
calculateTrialDay,
checkForTrialEnding,
paymentExpired,
checkForFreeOrgOverflow,
checkForPostReleaseProModal,
@@ -248,7 +247,7 @@
if (isCloud) {
if (!$page.url.pathname.includes('/console/onboarding')) {
const orgs = await sdk.forConsole.teams.list([
Query.equal('billingPlan', 'tier-0')
Query.equal('billingPlan', BillingPlan.STARTER)
]);
checkForPostReleaseProModal(orgs);
@@ -282,9 +281,8 @@
if (!org) return;
if (isCloud) {
calculateTrialDay(org);
checkForTrialEnding(org);
await paymentExpired(org);
checkForUsageLimit(org);
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
await checkPaymentAuthorizationRequired(org);
}
+8 -2
View File
@@ -1,4 +1,6 @@
import { Dependencies } from '$lib/constants';
import type { Plan } from '$lib/sdk/billing';
import type { Tier } from '$lib/stores/billing';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
@@ -18,9 +20,13 @@ export const load: LayoutLoad = async ({ fetch, depends }) => {
const [data, variables] = await Promise.all([versionPromise, variablesPromise]);
let plansInfo = null;
let plansInfo = new Map<Tier, Plan>();
if (isCloud) {
plansInfo = await sdk.forConsole.billing.getPlansInfo();
const plansArray = await sdk.forConsole.billing.getPlansInfo();
plansInfo = plansArray.plans.reduce((map, plan) => {
map.set(plan.$id as Tier, plan);
return map;
}, new Map<Tier, Plan>());
}
return {
+3 -1
View File
@@ -7,7 +7,9 @@ export const load: PageLoad = async ({ parent, url }) => {
if (organizations.total) {
const teamId = account.prefs.organization ?? organizations.teams[0].$id;
throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
if (!teamId) {
throw redirect(303, `${base}/console/account/organizations${url.search ?? ''}`);
} else throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
} else {
throw redirect(303, `${base}/console/onboarding${url.search ?? ''}`);
}
+1 -1
View File
@@ -11,7 +11,7 @@
async function deleteAccount() {
try {
await sdk.forConsole.account.updateStatus();
await sdk.forConsole.account.delete();
await invalidate(Dependencies.ACCOUNT);
showDelete = false;
addNotification({
@@ -22,6 +22,7 @@
import { toLocaleDate } from '$lib/helpers/date';
import { wizard } from '$lib/stores/wizard';
import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte';
import { BillingPlan } from '$lib/constants';
export let data: PageData;
let addOrganization = false;
@@ -71,7 +72,7 @@
</svelte:fragment>
<svelte:fragment slot="status">
{#if isCloudOrg(organization)}
{#if organization?.billingPlan === 'tier-0'}
{#if organization?.billingPlan === BillingPlan.STARTER}
<div
class="u-flex u-cross-center"
use:tooltip={{
@@ -86,10 +87,10 @@
class="u-flex u-cross-center"
use:tooltip={{
content: `Your trial ends on ${toLocaleDate(
organization.billingTrialEndDate
organization.billingStartDate
)}. ${$daysLeftInTrial} days remaining.`
}}>
<Pill>FREE TRIAL</Pill>
<Pill>TRIAL</Pill>
</div>
{/if}
{/if}
@@ -17,7 +17,7 @@
isUpgrade
} from './wizard/cloudOrganizationChangeTier/store';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { page } from '$app/stores';
import { organization } from '$lib/stores/organization';
@@ -35,12 +35,13 @@
async function changeTier() {
//Downgrade
if ($changeOrganizationTier.billingPlan === 'tier-0') {
if ($changeOrganizationTier.billingPlan === BillingPlan.STARTER) {
try {
await sdk.forConsole.billing.updatePlan(
$organization.$id,
$changeOrganizationTier.billingPlan,
$changeOrganizationTier.paymentMethodId
$changeOrganizationTier.paymentMethodId,
$changeOrganizationTier.billingAddressId
);
feedback.submitFeedback(
'downgrade',
@@ -60,7 +61,6 @@
wizard.hide();
trackEvent(Submit.OrganizationDowngrade, {
customId: !!$changeOrganizationTier.id,
plan: tierToPlan($changeOrganizationTier.billingPlan)?.name
});
} catch (e) {
@@ -78,35 +78,10 @@
const org = await sdk.forConsole.billing.updatePlan(
$organization.$id,
$changeOrganizationTier.billingPlan,
$changeOrganizationTier.paymentMethodId
$changeOrganizationTier.paymentMethodId,
$changeOrganizationTier.billingAddressId
);
//Add billing address
if ($changeOrganizationTier.billingAddressId) {
await sdk.forConsole.billing.setBillingAddress(
org.$id,
$changeOrganizationTier.billingAddressId
);
} else if (
$changeOrganizationTier.billingAddress &&
$changeOrganizationTier.billingAddress.streetAddress
) {
const response = await sdk.forConsole.billing.createAddress(
$changeOrganizationTier.billingAddress.country,
$changeOrganizationTier.billingAddress.streetAddress,
$changeOrganizationTier.billingAddress.city,
$changeOrganizationTier.billingAddress.state,
$changeOrganizationTier.billingAddress.postalCode
? $changeOrganizationTier.billingAddress.postalCode
: undefined,
$changeOrganizationTier.billingAddress.addressLine2
? $changeOrganizationTier.billingAddress.addressLine2
: undefined
);
await sdk.forConsole.billing.setBillingAddress(org.$id, response.$id);
}
//Add coupon
if ($changeOrganizationTier.couponCode) {
await sdk.forConsole.billing.addCredit(
@@ -168,7 +143,6 @@
});
}
trackEvent($isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade, {
customId: !!$changeOrganizationTier.id,
plan: tierToPlan($changeOrganizationTier.billingPlan)?.name
});
wizard.hide();
@@ -188,20 +162,10 @@
onDestroy(() => {
$changeOrganizationTier = {
id: null,
billingPlan: 'tier-1',
billingPlan: BillingPlan.PRO,
paymentMethodId: null,
collaborators: [],
billingAddressId: null,
billingAddress: {
$id: null,
streetAddress: null,
addressLine2: null,
city: null,
state: null,
postalCode: null,
country: null
},
taxId: null,
feedbackMessage: null
};
@@ -212,7 +176,7 @@
component: ChoosePlan
});
$changeTierSteps.set(2, {
label: 'Payment details',
label: 'Payment',
component: PaymentDetails
});
$changeTierSteps.set(3, {
@@ -240,4 +204,5 @@
title="Change plan"
steps={$changeTierSteps}
finalAction={$changeOrganizationFinalAction}
on:exit={onFinish} />
on:exit={onFinish}
confirmExit />
@@ -13,7 +13,7 @@
createOrgSteps
} from './wizard/cloudOrganization/store';
import { goto, invalidate, preloadData } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { ID } from '@appwrite.io/console';
import { page } from '$app/stores';
@@ -28,37 +28,24 @@
async function create() {
try {
// Create free organization if coming from onboarding
if ($page.url.pathname.includes('/console/onboarding')) {
await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal Projects',
BillingPlan.STARTER,
null,
null
);
}
const org = await sdk.forConsole.billing.createOrganization(
$createOrganization.id ?? ID.unique(),
$createOrganization.name,
$createOrganization.billingPlan,
$createOrganization.paymentMethodId
$createOrganization.paymentMethodId,
$createOrganization.billingAddressId
);
//Add billing address
if ($createOrganization.billingAddressId) {
await sdk.forConsole.billing.setBillingAddress(
org.$id,
$createOrganization.billingAddressId
);
} else if (
$createOrganization.billingAddress &&
$createOrganization.billingAddress.streetAddress
) {
const response = await sdk.forConsole.billing.createAddress(
$createOrganization.billingAddress.country,
$createOrganization.billingAddress.streetAddress,
$createOrganization.billingAddress.city,
$createOrganization.billingAddress.state,
$createOrganization.billingAddress.postalCode
? $createOrganization.billingAddress.postalCode
: undefined,
$createOrganization.billingAddress.addressLine2
? $createOrganization.billingAddress.addressLine2
: undefined
);
await sdk.forConsole.billing.setBillingAddress(org.$id, response.$id);
}
//Add budget
if ($createOrganization?.billingBudget) {
@@ -88,6 +75,13 @@
await sdk.forConsole.billing.updateTaxId(org.$id, $createOrganization.taxId);
}
trackEvent(Submit.OrganizationCreate, {
customId: !!$createOrganization.id,
plan: tierToPlan($createOrganization.billingPlan)?.name,
budget_cap_enabled: !!$createOrganization?.billingBudget,
members_invited: $createOrganization?.collaborators?.length
});
await invalidate(Dependencies.ACCOUNT);
await preloadData(`/console/organization-${org.$id}`);
await goto(`/console/organization-${org.$id}`);
@@ -95,14 +89,9 @@
type: 'success',
message: `${$createOrganization.name ?? 'Organization'} has been created`
});
trackEvent(Submit.OrganizationCreate, {
customId: !!$createOrganization.id,
plan: tierToPlan($createOrganization.billingPlan)?.name,
budget_cap_enabled: !!$createOrganization?.billingBudget,
members_invited: $createOrganization?.collaborators?.length
});
wizard.hide();
if (org.billingPlan === 'tier-1') {
if (org.billingPlan === BillingPlan.PRO) {
wizard.showCover(HoodieCover);
}
} catch (e) {
@@ -117,19 +106,10 @@
$createOrganization = {
id: null,
name: null,
billingPlan: 'tier-1',
billingPlan: BillingPlan.PRO,
paymentMethodId: null,
collaborators: [],
billingAddressId: null,
billingAddress: {
$id: null,
streetAddress: null,
addressLine2: null,
city: null,
state: null,
postalCode: null,
country: null
},
taxId: null
};
});
@@ -139,7 +119,7 @@
component: OrganizationDetails
});
$createOrgSteps.set(2, {
label: 'Payment details',
label: 'Payment',
component: PaymentDetails
});
$createOrgSteps.set(3, {
@@ -162,4 +142,5 @@
title="Create organization"
steps={$createOrgSteps}
finalAction={$createOrganizationFinalAction}
on:exit={onFinish} />
on:exit={onFinish}
confirmExit />
+107 -44
View File
@@ -2,11 +2,11 @@
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Card } from '$lib/components';
import { Card, Heading } from '$lib/components';
import CustomId from '$lib/components/customId.svelte';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button, Form, InputText } from '$lib/elements/forms';
import { Button, Form, InputSelect, InputText } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
@@ -16,10 +16,18 @@
import { ID } from '@appwrite.io/console';
import { onMount } from 'svelte';
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
import { tierToPlan, type Tier } from '$lib/stores/billing';
import { createOrganization } from '../wizard/cloudOrganization/store';
let name: string;
let id: string;
let showCustomId = false;
let plan: Tier;
const options = [
{ value: BillingPlan.STARTER, label: 'Starter - $0/month' },
{ value: BillingPlan.PRO, label: 'Pro - $15/month + add-ons' }
];
onMount(() => {
if (isCloud) {
@@ -32,65 +40,120 @@
}
});
async function createProject() {
try {
const org = await createOrganization();
const project = await sdk.forConsole.projects.create(
id ?? ID.unique(),
name,
org.$id,
isCloud ? 'eu-de' : 'default'
);
await invalidate(Dependencies.ACCOUNT);
goto(`/console/project-${project.$id}`);
trackEvent(Submit.ProjectCreate, {
customId: !!id,
teamId: org.$id
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.ProjectCreate);
}
}
async function createOrganization() {
async function handleSubmit() {
const orgName = name?.length ? name : 'Personal Projects';
if (isCloud) {
return await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal Projects',
'tier-0',
null
);
} else return await sdk.forConsole.teams.create(ID.unique(), 'Personal Projects');
if (plan === BillingPlan.STARTER) {
try {
const org = await sdk.forConsole.billing.createOrganization(
id ?? ID.unique(),
orgName,
plan,
null,
null
);
trackEvent(Submit.OrganizationCreate, {
customId: !!id,
plan: tierToPlan(plan)?.name
});
await invalidate(Dependencies.ACCOUNT);
await goto(`/console/organization-${org.$id}`);
addNotification({
message: `${orgName} organization successfully created`,
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.OrganizationCreate);
}
} else {
wizard.start(CreateOrganizationCloud, null, 2);
$createOrganization.name = orgName;
$createOrganization.billingPlan = plan;
$createOrganization.id = id;
}
} else {
try {
const org = await sdk.forConsole.teams.create(id ?? ID.unique(), orgName);
await invalidate(Dependencies.ACCOUNT);
await goto(`/console/organization-${org.$id}`);
addNotification({
message: `${orgName} organization successfully created`,
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.OrganizationCreate);
}
}
}
</script>
<Container overlapCover size="large">
<Card>
<Form onSubmit={createProject}>
<FormList>
<Heading size="4" tag="h2">Create a new organization</Heading>
<Form onSubmit={handleSubmit}>
<FormList gap={16}>
<InputText
id="name"
label="Project name"
placeholder="First Appwrite Project"
required
label="Name"
placeholder="Organization name"
hideRequired
bind:value={name} />
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
Project ID
Organization ID
</span>
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
<CustomId bind:show={showCustomId} name="Organization" isProject bind:id />
{/if}
<Button fullWidth submit disabled={name === ''} event="create_project">
Create project
{#if isCloud}
<div class="u-margin-block-start-8">
<h3><b>Plan</b></h3>
<p>
For more details on our plans, visit our <Button
link
external
href="https://appwrite.io/pricing">pricing page</Button
>.
</p>
<FormList class="u-margin-block-start-8">
<InputSelect
placeholder="Select a plan"
label="Select plan"
showLabel={false}
id="plan"
required
hideRequired
{options}
bind:value={plan} />
</FormList>
</div>
{/if}
<Button
fullWidth
submit
disabled={isCloud && !plan}
event="create_organization"
submissionLoader
let:isSubmitting>
{#if isSubmitting}
Creating your first organization
{:else}
Get started
{/if}
</Button>
</FormList>
</Form>
+5 -2
View File
@@ -1,7 +1,10 @@
<script lang="ts">
import { Cover, CoverTitle } from '$lib/layout';
import { Heading } from '$lib/components';
import { Cover } from '$lib/layout';
</script>
<Cover size="large">
<CoverTitle>Let's create your first project</CoverTitle>
<div class="u-flex u-cross-center u-main-center u-width-full-line">
<Heading size="1" tag="h1" trimmed={false}>Welcome to Appwrite</Heading>
</div>
</Cover>
@@ -9,9 +9,11 @@ import Header from './header.svelte';
import { headerAlert } from '$lib/stores/headerAlert';
import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte';
import { get } from 'svelte/store';
import { preferences } from '$lib/stores/preferences';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.ORGANIZATION);
depends(Dependencies.MEMBERS);
depends(Dependencies.PAYMENT_METHODS);
if (isCloud) {
@@ -28,6 +30,10 @@ export const load: LayoutLoad = async ({ params, depends }) => {
}
try {
const prefs = await sdk.forConsole.account.getPrefs();
const newPrefs = { ...prefs, organization: params.organization };
sdk.forConsole.account.updatePrefs(newPrefs);
preferences.loadTeamPrefs(params.organization);
return {
header: Header,
breadcrumbs: Breadcrumbs,
@@ -35,6 +41,9 @@ export const load: LayoutLoad = async ({ params, depends }) => {
members: await sdk.forConsole.teams.listMemberships(params.organization)
};
} catch (e) {
const prefs = await sdk.forConsole.account.getPrefs();
const newPrefs = { ...prefs, organization: null };
sdk.forConsole.account.updatePrefs(newPrefs);
localStorage.removeItem('organization');
throw error(e.code, e.message);
}
@@ -31,6 +31,7 @@
import type { RegionList } from '$lib/sdk/billing';
import { onMount } from 'svelte';
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
import { organization } from '$lib/stores/organization';
export let data;
@@ -136,93 +137,96 @@
}
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Projects</Heading>
{#if $organization?.$id}
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Projects</Heading>
<DropList bind:show={showDropdown} placement="bottom-end">
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create project</span>
</Button>
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
<DropList bind:show={showDropdown} placement="bottom-end">
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create project</span>
</Button>
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
{#if data.projects.total}
<CardContainer
total={data.projects.total}
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
<li>
<GridItem1 href={`${base}/console/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
</p>
{/if}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
{#each platforms as platform, i}
{#if i < 3}
{#if data.projects.total}
<CardContainer
total={data.projects.total}
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
<li>
<GridItem1 href={`${base}/console/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
</p>
{/if}
{#each platforms as platform, i}
{#if i < 3}
<Pill>
<span class={`icon-${platform.icon}`} aria-hidden="true" />
{platform.name}
</Pill>
{/if}
{/each}
{#if platforms?.length > 3}
<Pill>
<span class={`icon-${platform.icon}`} aria-hidden="true" />
{platform.name}
+{project.platforms.length - 3}
</Pill>
{/if}
{/each}
{#if platforms?.length > 3}
<Pill>
+{project.platforms.length - 3}
</Pill>
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray">
{region.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
</li>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray u-medium u-line-height-2">
{region?.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
</li>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
</Container>
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
</Container>
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
{/if}
@@ -17,7 +17,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
projects: await sdk.forConsole.projects.list([
Query.offset(offset),
Query.equal('teamId', params.organization),
Query.limit(CARD_LIMIT),
Query.limit(limit),
Query.orderDesc('')
])
};
@@ -19,6 +19,7 @@
import { toLocaleDate } from '$lib/helpers/date';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { BillingPlan } from '$lib/constants';
$: defaultPaymentMethod = $paymentMethods?.paymentMethods?.find(
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
@@ -95,7 +96,7 @@
<BillingAddress />
<TaxId />
<BudgetCap />
{#if $organization?.billingPlan !== 'tier-0' && !!$organization?.billingBudget}
{#if $organization?.billingPlan !== BillingPlan.STARTER && !!$organization?.billingBudget}
<BudgetAlert />
{/if}
<AvailableCredit />
@@ -155,8 +155,12 @@
</svelte:fragment>
</CardGrid>
<AddressModal bind:show={showCreate} organization={$organization?.$id} />
<EditAddressModal bind:show={showEdit} bind:selectedAddress={billingAddress} />
{#if showCreate}
<AddressModal bind:show={showCreate} organization={$organization?.$id} />
{/if}
{#if showEdit}
<EditAddressModal bind:show={showEdit} bind:selectedAddress={billingAddress} />
{/if}
{#if showReplace}
<ReplaceAddress bind:show={showReplace} />
{/if}
@@ -2,7 +2,7 @@
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Alert, CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputNumber, InputSwitch } from '$lib/elements/forms';
import { showUsageRatesModal } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
@@ -59,7 +59,7 @@
class="link">Learn more about usage rates.</button>
</p>
<svelte:fragment slot="aside">
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Alert type="info">
<svelte:fragment slot="title">
Budget caps are a Pro plan feature
@@ -74,7 +74,12 @@
</Alert>
{:else}
<FormList>
<InputSwitch id="capActive" label="Enable budget cap" bind:value={capActive} />
<InputSwitch id="cap-active" label="Enable budget cap" bind:value={capActive}>
<svelte:fragment slot="description">
Budget cap limits do not include the base amount of your plan. Cap usage
is reset at the beginning of each billing cycle.
</svelte:fragment>
</InputSwitch>
{#if capActive}
<InputNumber
placeholder="Add budget cap"
@@ -14,6 +14,7 @@
import { Query } from '@appwrite.io/console';
import { abbreviateNumber, formatNumberWithCommas } from '$lib/helpers/numbers';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { BillingPlan } from '$lib/constants';
let currentInvoice: Invoice;
const today = new Date();
@@ -25,9 +26,9 @@
currentInvoice = invoices.invoices[0];
});
$: currentPlan = $plansInfo.plans.find((p) => p.$id === $organization?.billingPlan);
$: currentPlan = $plansInfo?.get($organization?.billingPlan);
$: extraUsage = currentInvoice?.amount - currentPlan?.price;
$: isTrial = new Date($organization?.billingTrialEndDate).getTime() - today.getTime() > 0;
$: isTrial = new Date($organization?.billingStartDate).getTime() - today.getTime() > 0;
</script>
{#if $organization}
@@ -51,8 +52,8 @@
<h6 class="body-text-1 u-bold u-trim-1">
{tierToPlan($organization?.billingPlan)?.name} plan
</h6>
{#if $organization?.billingPlan !== 'tier-0' && isTrial}
<Pill>FREE TRIAL</Pill>
{#if $organization?.billingPlan !== BillingPlan.STARTER && isTrial}
<Pill>TRIAL</Pill>
{/if}
</div>
@@ -65,7 +66,7 @@
</span>
</p>
</div>
{#if currentInvoice?.usage?.length && $organization?.billingPlan !== 'tier-0' && !isTrial}
{#if currentInvoice?.usage?.length && $organization?.billingPlan !== BillingPlan.STARTER && !isTrial}
{@const extraMembers = currentInvoice.usage.find((u) => u.name === 'members')}
{#if extraMembers}
<div class="u-margin-block-start-24">
@@ -128,7 +129,7 @@
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<div class="u-flex u-gap-16 u-flex-wrap">
<Button text href={`${base}/console/organization-${$organization?.$id}/usage`}>
View estimated usage
@@ -4,8 +4,8 @@
$: breadcrumbs = [
{
href: `/console/organization-${$organization.$id}`,
title: $organization.name
href: `/console/organization-${$organization?.$id}`,
title: $organization?.name
}
];
</script>
@@ -7,7 +7,7 @@
import { createEventDispatcher } from 'svelte';
import { organization } from '$lib/stores/organization';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { isCloud } from '$lib/system';
import { plansInfo } from '$lib/stores/billing';
@@ -17,7 +17,7 @@
const dispatch = createEventDispatcher();
const url = `${$page.url.origin}/invite`;
$: plan = $plansInfo?.plans?.find((p) => p.$id === $organization?.billingPlan);
$: plan = $plansInfo?.get($organization?.billingPlan);
let email: string, name: string, error: string;
@@ -34,6 +34,8 @@
);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.ORGANIZATION);
await invalidate(Dependencies.MEMBERS);
showCreate = false;
addNotification({
type: 'success',
@@ -57,9 +59,9 @@
<Modal title="Invite Member" {error} size="big" bind:show={showCreate} onSubmit={create}>
{#if isCloud}
<Alert type="info">
{#if $organization?.billingPlan === 'tier-2'}
{#if $organization?.billingPlan === BillingPlan.SCALE}
You can add unlimited organization members on the {plan.name} plan at no cost.
{:else if $organization?.billingPlan === 'tier-1'}
{:else if $organization?.billingPlan === BillingPlan.PRO}
You can add unlimited organization members on the {plan.name} plan for
<b>${plan.addons.member.price} each per billing period</b>.
{/if}
@@ -52,17 +52,17 @@
$createProject = {
id: null,
name: null,
region: 'eu-de'
region: 'fra'
};
});
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Project details',
label: 'Details',
component: Step1
});
stepsComponents.set(2, {
label: 'Select region',
label: 'Region',
component: Step2
});
</script>
@@ -1,6 +1,6 @@
<script lang="ts">
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { goto, invalidate } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
@@ -9,6 +9,10 @@
import { createEventDispatcher } from 'svelte';
import { user } from '$lib/stores/user';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { checkForUsageLimit } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
const dispatch = createEventDispatcher();
@@ -25,6 +29,11 @@
} else {
dispatch('deleted');
}
invalidate(Dependencies.ACCOUNT);
invalidate(Dependencies.MEMBERS);
if (isCloud && $organization) {
await checkForUsageLimit($organization);
}
showDelete = false;
addNotification({
type: 'success',
@@ -13,9 +13,10 @@
import ChangeOrganizationTierCloud from '../changeOrganizationTierCloud.svelte';
import { goto } from '$app/navigation';
import { last } from '$lib/helpers/array';
import { BillingPlan } from '$lib/constants';
export let show = false;
const plan = $plansInfo.plans.find((plan) => plan.$id === $organization.billingPlan);
const plan = $plansInfo?.get($organization.billingPlan);
let usage: OrganizationUsage = null;
let members: Models.MembershipList = null;
let excess: Record<string, number> = null;
@@ -62,9 +63,9 @@
Appwrite Pro is now available. To facilitate a smooth transition for your projects, Starter plan
will maintain its current state of unlimited resource usage. This extension will be in effect
until January 15th, 2024.
until January 31st, 2024.
{#if $organization.billingPlan === 'tier-0'}
{#if $organization.billingPlan === BillingPlan.STARTER}
<p class="text">
Usage for <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
$organization.billingPlan
@@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { tooltip } from '$lib/actions/tooltip';
import { AvatarGroup, DropList, DropListItem, DropListLink, Tab, Tabs } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
@@ -80,86 +81,88 @@
: permanentTabs;
</script>
<Cover>
<svelte:fragment slot="header">
<DropList bind:show={showDropdown} placement="bottom-start" noArrow scrollable>
<button
class="button is-text u-padding-inline-0"
on:click={() => (showDropdown = !showDropdown)}>
<h1 class="heading-level-4 u-flex u-cross-center u-gap-8">
<span class="u-flex u-cross-center u-gap-8">
{$organization.name}
{#if isCloud && $organization?.billingPlan === 'tier-0'}
<Pill>FREE</Pill>
{/if}
{#if isCloud && $organization?.billingTrialStartDate && $daysLeftInTrial > 0}
<div
class="u-flex u-cross-center"
use:tooltip={{
content: `Your trial ends on ${toLocaleDate(
$organization.billingTrialEndDate
)}. ${$daysLeftInTrial} days remaining.`
}}>
<Pill>FREE TRIAL</Pill>
</div>
{/if}
</span>
<span
class={`icon-cheveron-${showDropdown ? 'up' : 'down'}`}
aria-hidden="true" />
</h1>
</button>
<svelte:fragment slot="list">
{#each $organizationList.teams as org}
<DropListLink
href={`${base}/console/organization-${org.$id}`}
on:click={() => (showDropdown = false)}>
{org.name}
</DropListLink>
{/each}
</svelte:fragment>
<svelte:fragment slot="other">
<section class="drop-section">
<ul class="drop-list">
<DropListItem icon="plus" on:click={createOrg}>
New Organization
</DropListItem>
</ul>
</section></svelte:fragment>
</DropList>
<div class="u-margin-inline-start-auto">
<div class="u-flex u-gap-16 u-cross-center">
<a href={`${path}/members`} class="is-not-mobile">
<AvatarGroup size={40} {avatars} total={$members?.total ?? 0} />
</a>
<div
use:tooltip={{
content:
$organization?.billingPlan === 'tier-0'
? `Upgrade to add more members`
: `You've reached the members limit for the ${
tierToPlan($organization?.billingPlan)?.name
} plan`,
disabled: !areMembersLimited
}}>
<Button
secondary
on:click={() => newMemberModal.set(true)}
disabled={areMembersLimited}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Invite</span>
</Button>
{#if $organization?.$id}
<Cover>
<svelte:fragment slot="header">
<DropList bind:show={showDropdown} placement="bottom-start" noArrow scrollable>
<button
class="button is-text u-padding-inline-0"
on:click={() => (showDropdown = !showDropdown)}>
<h1 class="heading-level-4 u-flex u-cross-center u-gap-8">
<span class="u-flex u-cross-center u-gap-8">
{$organization.name}
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER}
<Pill>FREE</Pill>
{/if}
{#if isCloud && $organization?.billingTrialStartDate && $daysLeftInTrial > 0}
<div
class="u-flex u-cross-center"
use:tooltip={{
content: `Your trial ends on ${toLocaleDate(
$organization.billingStartDate
)}. ${$daysLeftInTrial} days remaining.`
}}>
<Pill>TRIAL</Pill>
</div>
{/if}
</span>
<span
class={`icon-cheveron-${showDropdown ? 'up' : 'down'}`}
aria-hidden="true" />
</h1>
</button>
<svelte:fragment slot="list">
{#each $organizationList.teams as org}
<DropListLink
href={`${base}/console/organization-${org.$id}`}
on:click={() => (showDropdown = false)}>
{org.name}
</DropListLink>
{/each}
</svelte:fragment>
<svelte:fragment slot="other">
<section class="drop-section">
<ul class="drop-list">
<DropListItem icon="plus" on:click={createOrg}>
New Organization
</DropListItem>
</ul>
</section></svelte:fragment>
</DropList>
<div class="u-margin-inline-start-auto">
<div class="u-flex u-gap-16 u-cross-center">
<a href={`${path}/members`} class="is-not-mobile">
<AvatarGroup size={40} {avatars} total={$members?.total ?? 0} />
</a>
<div
use:tooltip={{
content:
$organization?.billingPlan === BillingPlan.STARTER
? `Upgrade to add more members`
: `You've reached the members limit for the ${
tierToPlan($organization?.billingPlan)?.name
} plan`,
disabled: !areMembersLimited
}}>
<Button
secondary
on:click={() => newMemberModal.set(true)}
disabled={areMembersLimited}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Invite</span>
</Button>
</div>
</div>
</div>
</div></svelte:fragment>
<Tabs>
{#each tabs as tab}
<Tab
href={tab.href}
selected={isTabSelected(tab, $page.url.pathname, path, tabs)}
event={tab.event}>
{tab.title}
</Tab>
{/each}
</Tabs>
</Cover>
</div></svelte:fragment>
<Tabs>
{#each tabs as tab}
<Tab
href={tab.href}
selected={isTabSelected(tab, $page.url.pathname, path, tabs)}
event={tab.event}>
{tab.title}
</Tab>
{/each}
</Tabs>
</Cover>
{/if}
@@ -1,9 +1,7 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { AvatarInitials, PaginationWithLimit } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
@@ -29,7 +27,6 @@
let selectedMember: Models.Membership;
let showDelete = false;
const url = `${$page.url.origin}/console/`;
const deleted = () => invalidate(Dependencies.ACCOUNT);
const resend = async (member: Models.Membership) => {
try {
await sdk.forConsole.teams.createMembership(
@@ -124,4 +121,4 @@
{/if}
</Container>
<Delete {selectedMember} bind:showDelete on:deleted={() => deleted()} />
<Delete {selectedMember} bind:showDelete />
@@ -6,6 +6,7 @@ import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, params, route, depends }) => {
depends(Dependencies.ORGANIZATION);
depends(Dependencies.MEMBERS);
const page = getPage(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
@@ -8,8 +8,10 @@
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { onMount } from 'svelte';
import Delete from './deleteOrganization.svelte';
import Delete from './deleteOrganizationModal.svelte';
import DownloadDPA from './downloadDPA.svelte';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { isCloud } from '$lib/system';
export let data;
let name: string;
@@ -63,6 +65,10 @@
</CardGrid>
</Form>
{#if isCloud}
<DownloadDPA />
{/if}
<CardGrid danger>
<div>
<Heading tag="h6" size="7">Delete organization</Heading>
@@ -0,0 +1,54 @@
<script lang="ts">
import { Box, CardGrid, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent } from '$lib/actions/analytics';
async function downloadPdf() {
trackEvent(Submit.DownloadDPA);
const today = new Date().toISOString();
const prefs = await sdk.forConsole.account.getPrefs();
const newPrefs = { ...prefs, DPA: today };
sdk.forConsole.account.updatePrefs(newPrefs);
}
</script>
<CardGrid>
<div>
<Heading tag="h6" size="7">Download DPA document</Heading>
</div>
<p class="text">
After downloading, have the DPA signed by your organization's compliance authority, such as
your CEO or Compliance Manager, and submit it to <a
class="link"
href="mailto:privacy@appwrite.io">privacy@appwrite.io</a
>.
</p>
<svelte:fragment slot="aside">
<Box>
<h6>
<b>Data Processing Agreement (DPA) document</b>
</h6>
<p class="text u-margin-block-start-8">
The DPA is a legal document that describes the roles and responsibilities of
Appwrite and the organization when personal data is processed. <a
class="link"
target="_blank"
rel="noopener noreferrer"
href="https://appwrite.io/docs/advanced/security/gdpr#dpa"
>Learn more about the DPA</a
>.
</p>
<Button
secondary
external
class="u-margin-block-start-16"
on:click={downloadPdf}
href="/legal/dpa.pdf"
event="download_dpa">
<span class="icon-download" aria-hidden="true" />
<span class="text">Download</span>
</Button>
</Box>
</svelte:fragment>
</CardGrid>
@@ -12,6 +12,7 @@
import { formatNum } from '$lib/helpers/string';
import { accumulateFromEndingTotal, total } from '$lib/layout/usage.svelte';
import type { OrganizationUsage } from '$lib/sdk/billing';
import { BillingPlan } from '$lib/constants';
export let data;
@@ -40,14 +41,14 @@
<div class="u-flex u-cross-center u-main-space-between">
<Heading tag="h2" size="5">Usage</Heading>
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
<span class="text">Upgrade</span>
</Button>
{/if}
</div>
<div class="u-flex u-main-space-between common-section u-cross-center">
{#if $organization.billingPlan === 'tier-2'}
{#if $organization.billingPlan === BillingPlan.SCALE}
<p class="text">
On the Scale plan, you'll be charged only for any usage that exceeds the thresholds
per resource listed below. <button
@@ -55,7 +56,7 @@
class="link"
type="button">Learn more about plan usage limits.</button>
</p>
{:else if $organization.billingPlan === 'tier-1'}
{:else if $organization.billingPlan === BillingPlan.PRO}
<p class="text">
On the Pro plan, you'll be charged only for any usage that exceeds the thresholds
per resource listed below. <button
@@ -63,7 +64,7 @@
class="link"
type="button">Learn more about plan usage limits.</button>
</p>
{:else if $organization.billingPlan === 'tier-0'}
{:else if $organization.billingPlan === BillingPlan.STARTER}
<p class="text">
If you exceed the limits of the {plan} plan, services for your organization's projects
may be disrupted.
@@ -43,7 +43,7 @@ export const load: PageLoad = async ({ params, parent }) => {
const queries: string[] = [];
if (usage.projects.length > 0) {
if (usage?.projects?.length > 0) {
queries.push(
Query.equal(
'$id',
@@ -3,12 +3,12 @@
import { Collapsible, CollapsibleItem } from '$lib/components';
import {
TableBody,
TableCell,
TableCellLink,
TableCellHead,
TableHeader,
TableRow,
Table
TableScroll,
TableCellText
} from '$lib/elements/table';
import { abbreviateNumber } from '$lib/helpers/numbers';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
@@ -55,29 +55,27 @@
<Collapsible>
<CollapsibleItem>
<svelte:fragment slot="title">Project breakdown</svelte:fragment>
<div class="table-wrapper" data-sveltekit-preload-data="off">
<Table noMargin noStyles>
<TableHeader>
<TableCellHead width={285}>Project</TableCellHead>
<TableCellHead>Usage</TableCellHead>
<TableCellHead width={140} />
</TableHeader>
<TableBody>
{#each groupByProject(metric).sort((a, b) => b.usage - a.usage) as project}
<TableRow>
<TableCell title="Project">
{getProjectName(project.projectId)}
</TableCell>
<TableCell title="Usage">{format(project.usage)}</TableCell>
<TableCellLink
title="Go to project usage"
href={getProjectUsageLink(project.projectId)}>
View project usage
</TableCellLink>
</TableRow>
{/each}
</TableBody>
</Table>
</div>
<TableScroll noMargin>
<TableHeader>
<TableCellHead width={185}>Project</TableCellHead>
<TableCellHead width={100}>Usage</TableCellHead>
<TableCellHead width={140} />
</TableHeader>
<TableBody>
{#each groupByProject(metric).sort((a, b) => b.usage - a.usage) as project}
<TableRow>
<TableCellText title="Project">
{getProjectName(project.projectId)}
</TableCellText>
<TableCellText title="Usage">{format(project.usage)}</TableCellText>
<TableCellLink
title="Go to project usage"
href={getProjectUsageLink(project.projectId)}>
View project usage
</TableCellLink>
</TableRow>
{/each}
</TableBody>
</TableScroll>
</CollapsibleItem>
</Collapsible>
@@ -62,7 +62,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Select region</svelte:fragment>
<svelte:fragment slot="title">Regions</svelte:fragment>
<svelte:fragment slot="subtitle">
Choose a deployment region for your project. This region cannot be changed.
</svelte:fragment>
@@ -7,5 +7,5 @@ export const createProject = writable<{
}>({
id: null,
name: null,
region: 'eu-de'
region: 'fra'
});
@@ -43,6 +43,7 @@
import { organization } from '$lib/stores/organization';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { wizard } from '$lib/stores/wizard';
import { BillingPlan } from '$lib/constants';
const projectId = $page.params.project;
@@ -117,7 +118,7 @@
</p>
<svelte:fragment slot="aside">
{#if $organization.billingPlan === 'tier-0'}
{#if $organization.billingPlan === BillingPlan.STARTER}
<Alert
buttons={[
{
@@ -3,7 +3,7 @@
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableScroll,
TableBody,
TableCell,
TableCellHead,
@@ -36,7 +36,7 @@
{/if}
</ContainerHeader>
{#if data.sessions.total}
<Table>
<TableScroll>
<TableHeader>
<TableCellHead width={140}>Browser and device</TableCellHead>
<TableCellHead width={140}>Session</TableCellHead>
@@ -85,7 +85,7 @@
</TableRow>
{/each}
</TableBody>
</Table>
</TableScroll>
{:else}
<EmptySearch>
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
@@ -68,9 +68,15 @@
<CardGrid>
<Heading tag="h6" size="7">Permissions</Heading>
<p>
Assign read or write permissions at the <b>collection level</b> or
<b>document level</b>. If collection level permissions are assigned, permissions applied
to individual documents are ignored.
A user requires appropriate permissions at either the <b>collection level</b> or
<b>document level</b> to access a document. If no permissions are configured, no user
can access the document
<a
href="https://appwrite.io/docs/products/databases/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about database permissions</a
>.
</p>
<svelte:fragment slot="aside">
@@ -40,9 +40,7 @@
import DeploymentCreatedBy from './deploymentCreatedBy.svelte';
import DeploymentDomains from './deploymentDomains.svelte';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { readOnly, tierToPlan } from '$lib/stores/billing';
import { hoursToDays } from '$lib/helpers/date';
import { organization } from '$lib/stores/organization';
import { readOnly } from '$lib/stores/billing';
export let data;
@@ -215,19 +213,7 @@
<TableCellHead width={80}>Size</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody service="logs" total={isCloud ? Infinity : 0}>
<svelte:fragment slot="limit" let:limit let:upgradeMethod>
<p class="text">
Logs are retained in rolling {hoursToDays(limit)} intervals with the
{tierToPlan($organization.billingPlan).name}
plan.
<button
class="link"
type="button"
on:click|preventDefault={upgradeMethod}>Upgrade</button> to increase
your log retention for a longer period.
</p>
</svelte:fragment>
<TableBody>
{#each $deploymentList.deployments as deployment, index (deployment.$id)}
{@const status = deployment.status}
<TableRow>
@@ -1,7 +1,7 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Alert, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
@@ -65,7 +65,7 @@
{logs} hour of logs
</li>
</ul>
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<p class="text">
<button class="link" type="button" on:click|preventDefault={upgradeMethod}
>Upgrade</button>
@@ -58,7 +58,7 @@
<Heading tag="h6" size="7" id="schedule">Schedule</Heading>
<p>
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
href="https://appwrite.io/docs/products/functions/execution"
href="https://appwrite.io/docs/products/functions/execution#schedule"
target="_blank"
rel="noopener noreferrer"
class="link">
@@ -8,7 +8,7 @@
<svelte:fragment slot="title">Schedule</svelte:fragment>
<svelte:fragment slot="subtitle">
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
href="https://appwrite.io/docs/products/functions/quick-start"
href="https://appwrite.io/docs/products/functions/execution#schedule"
target="_blank"
rel="noopener noreferrer"
class="link">More details on Cron syntax here</a
@@ -13,7 +13,7 @@
import InputPassword from '$lib/elements/forms/inputPassword.svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import InputNumber from '$lib/elements/forms/inputNumber.svelte';
@@ -124,7 +124,7 @@
class="link">here</a>
</p>
<svelte:fragment slot="aside">
{#if $organization.billingPlan === 'tier-0'}
{#if $organization.billingPlan === BillingPlan.STARTER}
<Alert type="info">
Custom SMTP is a Pro plan feature. Upgrade to enable custom SMTP sever.
<svelte:fragment slot="action">
@@ -182,14 +182,12 @@
id="username"
label="Username"
bind:value={username}
required
placeholder="Enter username" />
<InputPassword
showPasswordButton
id="passwort"
label="Password"
bind:value={password}
required
placeholder="Enter password" />
<InputChoice bind:value={secure} id="tls" label="TLS secure protocol">
@@ -202,7 +200,8 @@
<svelte:fragment slot="actions">
<Button
submit
disabled={isButtonDisabled || $organization.billingPlan === 'tier-0'}>
disabled={isButtonDisabled ||
$organization.billingPlan === BillingPlan.STARTER}>
Update
</Button>
</svelte:fragment>
@@ -19,6 +19,7 @@
import { formatNum } from '$lib/helpers/string';
import { total } from '$lib/layout/usage.svelte';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { BillingPlan } from '$lib/constants.js';
export let data;
@@ -51,28 +52,28 @@
<div class="u-flex u-cross-center u-main-space-between">
<Heading tag="h2" size="5">Usage</Heading>
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
<span class="text">Upgrade</span>
</Button>
{/if}
</div>
<div class="u-flex u-main-space-between common-section u-cross-center">
{#if $organization.billingPlan === 'tier-2'}
{#if $organization.billingPlan === BillingPlan.SCALE}
<p class="text">
On the Scale plan, you'll be charged only for any usage that exceeds the thresholds
per resource listed below. <Button
on:click={() => ($showUsageRatesModal = true)}
link>Learn more about plan usage limits.</Button>
</p>
{:else if $organization.billingPlan === 'tier-1'}
{:else if $organization.billingPlan === BillingPlan.PRO}
<p class="text">
On the Pro plan, you'll be charged only for any usage that exceeds the thresholds
per resource listed below. <Button
on:click={() => ($showUsageRatesModal = true)}
link>Learn more about plan usage limits.</Button>
</p>
{:else if $organization.billingPlan === 'tier-0'}
{:else if $organization.billingPlan === BillingPlan.STARTER}
<p class="text">
If you exceed the limits of the {plan} plan, services for your projects may be disrupted.
<button
@@ -53,7 +53,7 @@
const projectId = $page.params.project;
const bucketId = $page.params.bucket;
const usedStorage = bytesToSize(data.organizationUsage.storageTotal, 'MB');
const usedStorage = bytesToSize(data.organizationUsage.storageTotal, 'GB');
const getPreview = (fileId: string) =>
sdk.forProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
@@ -1,5 +1,6 @@
<script lang="ts">
import { Alert, CustomId } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button, FormList, InputFile } from '$lib/elements/forms';
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
@@ -28,12 +29,12 @@
The {plan.name} plan has a maximum upload file size limit of {Math.floor(
parseInt(size.value)
)}{size.unit}.
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
Upgrade to allow files of a larger size.
{/if}
</p>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<div class="alert-buttons u-flex">
<Button text on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
Upgrade plan
@@ -1,6 +1,7 @@
<script lang="ts">
import { Submit } from '$lib/actions/analytics';
import { Alert, CardGrid, Heading } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Button, Form, FormItem, InputNumber, InputSelect } from '$lib/elements/forms';
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
import { createByteUnitPair } from '$lib/helpers/unit';
@@ -42,12 +43,12 @@
The {plan.name} plan has a maximum upload file size limit of {Math.floor(
parseInt(size.value)
)}{size.unit}.
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
Upgrade to allow files of a larger size.
{/if}
</p>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<div class="alert-buttons u-flex">
<Button
text
@@ -85,7 +85,7 @@
{/if}
</div>
<InputFile bind:files allowedFileExtensions={['.env']} />
<InputFile bind:files />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
@@ -14,6 +14,30 @@
}
];
let addressList: AddressesList;
let country: string;
let streetAddress: string;
let city: string;
let state: string;
let postalCode: string;
let addressLine2: string;
async function handleAddress() {
if (!$createOrganization.billingAddressId) {
try {
const response = await sdk.forConsole.billing.createAddress(
country,
streetAddress,
city,
state,
postalCode ? postalCode : undefined,
addressLine2 ? addressLine2 : undefined
);
$createOrganization.billingAddressId = response.$id;
} catch (error) {
throw new Error(error.message);
}
}
}
onMount(async () => {
addressList = await sdk.forConsole.billing.listAddresses();
@@ -21,7 +45,7 @@
const countryList = await sdk.forProject.locale.listCountries();
const locale = await sdk.forProject.locale.get();
if (locale?.countryCode) {
$createOrganization.billingAddress.country = locale.countryCode;
country = locale.countryCode;
}
options = countryList.countries.map((country) => {
return {
@@ -32,7 +56,7 @@
});
</script>
<WizardStep>
<WizardStep beforeSubmit={handleAddress}>
<svelte:fragment slot="title">Billing address</svelte:fragment>
<svelte:fragment slot="subtitle">Add a billing address for your organization.</svelte:fragment>
@@ -70,25 +94,25 @@
placeholder="Enter tax ID"
optionalText="(optional)" />
<InputSelect
bind:value={$createOrganization.billingAddress.country}
bind:value={country}
{options}
label="Country or region"
placeholder="Select country or region"
id="country"
required />
<InputText
bind:value={$createOrganization.billingAddress.streetAddress}
bind:value={streetAddress}
id="address"
label="Street address"
placeholder="Enter street address"
required />
<InputText
bind:value={$createOrganization.billingAddress.addressLine2}
bind:value={addressLine2}
id="address2"
label="Address line 2"
placeholder="Unit number, floor, etc." />
<InputText
bind:value={$createOrganization.billingAddress.city}
bind:value={city}
id="city"
label="City or suburb"
placeholder="Enter your city"
@@ -97,7 +121,7 @@
<InputText
isMultiple
fullWidth
bind:value={$createOrganization.billingAddress.state}
bind:value={state}
id="state"
label="State"
placeholder="Enter your state"
@@ -105,7 +129,7 @@
<InputText
isMultiple
fullWidth
bind:value={$createOrganization.billingAddress.postalCode}
bind:value={postalCode}
id="zip"
label="Postal code"
placeholder="Enter postal code" />
@@ -1,5 +1,6 @@
<script lang="ts">
import { Box, CreditCardBrandImage } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Pill } from '$lib/elements';
import { toLocaleDate } from '$lib/helpers/date';
import { WizardStep } from '$lib/layout';
@@ -7,7 +8,7 @@
import { sdk } from '$lib/stores/sdk';
import { createOrganization, createOrganizationFinalAction } from './store';
const plan = $plansInfo.plans.find((p) => p.$id === $createOrganization.billingPlan);
const plan = $plansInfo?.get($createOrganization.billingPlan);
const collaboratorPrice = plan?.addons.member?.price ?? 0;
const collaboratorsNumber = $createOrganization?.collaborators?.length ?? 0;
const totalExpences = plan.price + collaboratorPrice * collaboratorsNumber;
@@ -29,12 +30,12 @@
if (!$createOrganization.billingPlan) {
throw new Error('Please select a plan.');
}
if ($createOrganization.billingPlan === 'tier-0') {
if ($createOrganization.billingPlan === BillingPlan.STARTER) {
$createOrganization.collaborators = [];
}
}
$: if ($createOrganization.billingPlan === 'tier-0') {
$: if ($createOrganization.billingPlan === BillingPlan.STARTER) {
$createOrganizationFinalAction = 'Create organization';
}
</script>
@@ -53,7 +54,7 @@
{/if}
</div>
{#if $createOrganization.billingPlan !== 'tier-0'}
{#if $createOrganization.billingPlan !== BillingPlan.STARTER}
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Additional members</p>
<p class="text u-margin-block-start-8">{collaboratorsNumber} members</p>
@@ -75,7 +76,7 @@
</div>
{/if}
<Box class="u-margin-block-start-32 u-flex u-flex-vertical u-gap-16" radius="small">
{#if $createOrganization.billingPlan !== 'tier-0'}
{#if $createOrganization.billingPlan !== BillingPlan.STARTER}
<span class="u-flex u-main-space-between">
<p class="text">{plan.name} plan</p>
<p class="text">${plan.price}</p>
@@ -91,7 +92,7 @@
<p class="text">${totalExpences}</p>
</span>
{#if $createOrganization.billingPlan !== 'tier-0'}
{#if $createOrganization.billingPlan !== BillingPlan.STARTER}
<p class="text u-margin-block-start-16">
This amount, and any additional usage fees, will be charged on a recurring 30-day
billing cycle after your trial period ends on <b
@@ -1,5 +1,6 @@
<script lang="ts">
import { Alert } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Button, Form, FormList, InputEmail } from '$lib/elements/forms';
import {
Table,
@@ -30,7 +31,7 @@
);
}
const plan = $plansInfo.plans.find((p) => p.$id === $createOrganization.billingPlan);
const plan = $plansInfo?.get($createOrganization.billingPlan);
</script>
<WizardStep>
@@ -41,10 +42,10 @@
</svelte:fragment>
<Alert type="info">
{#if $createOrganization.billingPlan === 'tier-2'}
{#if $createOrganization.billingPlan === BillingPlan.SCALE}
You can add unlimited organization members on the {plan.name} plan at no cost. Each member
added will receive an email invite to your organization on completion.
{:else if $createOrganization.billingPlan === 'tier-1'}
{:else if $createOrganization.billingPlan === BillingPlan.PRO}
You can add unlimited organization members on the {plan.name} plan for
<b>${plan.addons.member.price} each per month</b>. Each member added will receive an
email invite to your organization on completion.
@@ -70,7 +71,7 @@
<Table noStyles noMargin>
<TableHeader>
<TableCellHead>Collaborator</TableCellHead>
{#if $createOrganization.billingPlan === 'tier-1'}
{#if $createOrganization.billingPlan === BillingPlan.PRO}
<TableCellHead width={80}>Cost</TableCellHead>
{/if}
<TableCellHead width={40} />
@@ -79,7 +80,7 @@
{#each $createOrganization.collaborators as collaborator}
<TableRow>
<TableCellText title="collaborator">{collaborator}</TableCellText>
{#if $createOrganization.billingPlan === 'tier-1'}
{#if $createOrganization.billingPlan === BillingPlan.PRO}
<TableCellText title="cost">15$</TableCellText>
{/if}
<TableCell>
@@ -1,5 +1,6 @@
<script lang="ts">
import { CustomId, LabelCard } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Pill } from '$lib/elements';
import { InputText, FormList } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
@@ -11,27 +12,27 @@
let showCustomId = false;
$: anyOrgFree = $organizationList.teams?.find(
(org) => (org as Organization)?.billingPlan === 'tier-0'
(org) => (org as Organization)?.billingPlan === BillingPlan.STARTER
);
$: if ($createOrganization.billingPlan === 'tier-0' && $createOrgSteps) {
$: if ($createOrganization.billingPlan === BillingPlan.STARTER && $createOrgSteps) {
$createOrgSteps = updateStepStatus($createOrgSteps, 2, true);
$createOrgSteps = updateStepStatus($createOrgSteps, 3, true);
$createOrgSteps = updateStepStatus($createOrgSteps, 4, true);
}
$: if (
$createOrganization.billingPlan === 'tier-2' ||
$createOrganization.billingPlan === 'tier-1'
$createOrganization.billingPlan === BillingPlan.SCALE ||
$createOrganization.billingPlan === BillingPlan.PRO
) {
$createOrgSteps = updateStepStatus($createOrgSteps, 2, false);
$createOrgSteps = updateStepStatus($createOrgSteps, 3, false);
$createOrgSteps = updateStepStatus($createOrgSteps, 4, false);
}
$: freePlan = $plansInfo.plans.find((p) => p.$id === 'tier-0');
$: proPlan = $plansInfo.plans.find((p) => p.$id === 'tier-1');
$: scalePlan = $plansInfo.plans.find((p) => p.$id === 'tier-2');
$: freePlan = $plansInfo.get(BillingPlan.STARTER);
$: proPlan = $plansInfo.get(BillingPlan.PRO);
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
</script>
<WizardStep>
@@ -1,5 +1,5 @@
import { BillingPlan } from '$lib/constants';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import type { Address } from '$lib/sdk/billing';
import type { Tier } from '$lib/stores/billing';
import { writable } from 'svelte/store';
@@ -12,25 +12,15 @@ export const createOrganization = writable<{
billingPlan: Tier;
paymentMethodId: string;
billingAddressId: string;
billingAddress?: Address;
collaborators?: string[];
billingBudget?: number;
taxId?: string;
}>({
id: null,
name: null,
billingPlan: 'tier-1',
billingPlan: BillingPlan.PRO,
paymentMethodId: null,
collaborators: [],
billingAddressId: null,
billingAddress: {
$id: null,
streetAddress: null,
addressLine2: null,
city: null,
state: null,
postalCode: null,
country: null
},
taxId: null
});
@@ -12,13 +12,14 @@
import { toLocaleDate } from '$lib/helpers/date';
import { organization } from '$lib/stores/organization';
import { createOrganization } from './store';
import { plansInfo } from '$lib/stores/billing';
import { plansInfo, type Tier } from '$lib/stores/billing';
import { abbreviateNumber } from '$lib/helpers/numbers';
import { BillingPlan } from '$lib/constants';
export let show = false;
export let tier: string;
export let tier: Tier;
$: plan = $plansInfo.plans.find((p) => p.$id === tier);
$: plan = $plansInfo?.get(tier);
$: nextDate = $createOrganization?.name
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
@@ -49,7 +50,7 @@
}
];
$: isFree = tier === 'tier-0';
$: isFree = tier === BillingPlan.STARTER;
</script>
<Modal bind:show size="big" headerDivider={false} title="Usage rates">
@@ -57,12 +58,12 @@
Usage on the Starter plan is limited for the following resources. Next billing period: {toLocaleDate(
nextDate
)}.
{:else if tier === 'tier-1'}
{:else if tier === BillingPlan.PRO}
<p>
Usage on the Pro plan will be charged at the end of each billing period at the following
rates. Next billing period: {toLocaleDate(nextDate)}.
</p>
{:else if tier === 'tier-2'}
{:else if tier === BillingPlan.SCALE}
<p>
Usage on the Scale plan will be charged at the end of each billing period at the
following rates. Next billing period: {toLocaleDate(nextDate)}.
@@ -15,6 +15,30 @@
}
];
let addressList: AddressesList;
let country: string;
let streetAddress: string;
let city: string;
let state: string;
let postalCode: string;
let addressLine2: string;
async function handleAddress() {
if (!$changeOrganizationTier.billingAddressId) {
try {
const response = await sdk.forConsole.billing.createAddress(
country,
streetAddress,
city,
state,
postalCode ? postalCode : undefined,
addressLine2 ? addressLine2 : undefined
);
$changeOrganizationTier.billingAddressId = response.$id;
} catch (error) {
throw new Error(error.message);
}
}
}
onMount(async () => {
addressList = await sdk.forConsole.billing.listAddresses();
@@ -23,7 +47,7 @@
: addressList.billingAddresses?.[0]?.$id ?? null;
const locale = await sdk.forProject.locale.get();
if (locale?.countryCode && !$changeOrganizationTier.billingAddressId) {
$changeOrganizationTier.billingAddress.country = locale.countryCode;
country = locale.countryCode;
}
const countryList = await sdk.forProject.locale.listCountries();
options = countryList.countries.map((country) => {
@@ -35,7 +59,7 @@
});
</script>
<WizardStep>
<WizardStep beforeSubmit={handleAddress}>
<svelte:fragment slot="title">Billing address</svelte:fragment>
<svelte:fragment slot="subtitle">Add a billing address for your organization.</svelte:fragment>
@@ -73,25 +97,25 @@
placeholder="Enter tax ID"
optionalText="(optional)" />
<InputSelect
bind:value={$changeOrganizationTier.billingAddress.country}
bind:value={country}
{options}
label="Country or region"
placeholder="Select country or region"
id="country"
required />
<InputText
bind:value={$changeOrganizationTier.billingAddress.streetAddress}
bind:value={streetAddress}
id="address"
label="Street address"
placeholder="Enter street address"
required />
<InputText
bind:value={$changeOrganizationTier.billingAddress.addressLine2}
bind:value={addressLine2}
id="address2"
label="Address line 2"
placeholder="Unit number, floor, etc." />
<InputText
bind:value={$changeOrganizationTier.billingAddress.city}
bind:value={city}
id="city"
label="City or suburb"
placeholder="Enter your city"
@@ -100,7 +124,7 @@
<InputText
isMultiple
fullWidth
bind:value={$changeOrganizationTier.billingAddress.state}
bind:value={state}
id="state"
label="State"
placeholder="Enter your state"
@@ -108,7 +132,7 @@
<InputText
isMultiple
fullWidth
bind:value={$changeOrganizationTier.billingAddress.postalCode}
bind:value={postalCode}
id="zip"
label="Postal code"
placeholder="Enter postal code" />
@@ -11,19 +11,20 @@
import type { Models } from '@appwrite.io/console';
import { sizeToBytes } from '$lib/helpers/sizeConvertion';
import { Pill } from '$lib/elements';
import { BillingPlan } from '$lib/constants';
let usage: OrganizationUsage = null;
let members: Models.MembershipList = null;
$: if ($changeOrganizationTier.billingPlan === 'tier-0' && $changeTierSteps) {
$: if ($changeOrganizationTier.billingPlan === BillingPlan.STARTER && $changeTierSteps) {
$changeTierSteps = updateStepStatus($changeTierSteps, 2, true);
$changeTierSteps = updateStepStatus($changeTierSteps, 3, true);
$changeTierSteps = updateStepStatus($changeTierSteps, 4, true);
}
$: if (
$changeOrganizationTier.billingPlan === 'tier-2' ||
$changeOrganizationTier.billingPlan === 'tier-1'
$changeOrganizationTier.billingPlan === BillingPlan.SCALE ||
$changeOrganizationTier.billingPlan === BillingPlan.PRO
) {
$changeTierSteps = updateStepStatus($changeTierSteps, 2, false);
$changeTierSteps = updateStepStatus($changeTierSteps, 3, false);
@@ -37,9 +38,7 @@
function checkOverUsage() {
if (!usage) return;
const plan = $plansInfo.plans.find(
(plan) => plan.$id === $changeOrganizationTier.billingPlan
);
const plan = $plansInfo?.get($changeOrganizationTier.billingPlan);
const totBandwidth = usage?.bandwidth?.length > 0 ? usage.bandwidth[0].value : 0;
const totUsers = usage?.users?.length > 0 ? usage.users[0].value : 0;
@@ -57,17 +56,18 @@
members: members.total > plan.members ? members.total - (plan.members || Infinity) : 0
};
if (
$changeOrganizationTier.limitOverflow.bandwidth > 0 ||
$changeOrganizationTier.limitOverflow.storage > 0 ||
$changeOrganizationTier.limitOverflow.users > 0 ||
$changeOrganizationTier.limitOverflow.executions > 0 ||
$changeOrganizationTier.limitOverflow.members > 0
($changeOrganizationTier.limitOverflow.bandwidth > 0 ||
$changeOrganizationTier.limitOverflow.storage > 0 ||
$changeOrganizationTier.limitOverflow.users > 0 ||
$changeOrganizationTier.limitOverflow.executions > 0 ||
$changeOrganizationTier.limitOverflow.members > 0) &&
$changeOrganizationTier.billingPlan === BillingPlan.STARTER
) {
$changeOrganizationTier.isOverLimit = true;
$changeTierSteps = updateStepStatus($changeTierSteps, 4, false);
$changeTierSteps = updateStepStatus($changeTierSteps, 5, false);
} else {
$changeOrganizationTier.isOverLimit = false;
$changeTierSteps = updateStepStatus($changeTierSteps, 4, true);
$changeTierSteps = updateStepStatus($changeTierSteps, 5, true);
}
}
@@ -76,14 +76,14 @@
members = await sdk.forConsole.teams.listMemberships($organization.$id);
//Select closest tier from starting one
// if ($organization.billingPlan === 'tier-2') {
// $changeOrganizationTier.billingPlan = 'tier-1';
// if ($organization.billingPlan === BillingPlan.SCALE) {
// $changeOrganizationTier.billingPlan = BillingPlan.PRO;
// }
// else
if ($organization.billingPlan === 'tier-1') {
$changeOrganizationTier.billingPlan = 'tier-0';
} else if ($organization.billingPlan === 'tier-0') {
$changeOrganizationTier.billingPlan = 'tier-1';
if ($organization.billingPlan === BillingPlan.PRO) {
$changeOrganizationTier.billingPlan = BillingPlan.STARTER;
} else if ($organization.billingPlan === BillingPlan.STARTER) {
$changeOrganizationTier.billingPlan = BillingPlan.PRO;
}
});
@@ -91,14 +91,14 @@
if (!$changeOrganizationTier.billingPlan) {
throw new Error('Please select a plan.');
}
if ($changeOrganizationTier.billingPlan === 'tier-0') {
if ($changeOrganizationTier.billingPlan === BillingPlan.STARTER) {
$changeOrganizationTier.collaborators = [];
}
}
$: freePlan = $plansInfo.plans.find((p) => p.$id === 'tier-0');
$: proPlan = $plansInfo.plans.find((p) => p.$id === 'tier-1');
$: scalePlan = $plansInfo.plans.find((p) => p.$id === 'tier-2');
$: freePlan = $plansInfo?.get(BillingPlan.STARTER);
$: proPlan = $plansInfo?.get(BillingPlan.PRO);
$: scalePlan = $plansInfo?.get(BillingPlan.SCALE);
</script>
<WizardStep beforeSubmit={handleBefore}>
@@ -121,7 +121,7 @@
name="plan"
bind:group={$changeOrganizationTier.billingPlan}
value="tier-0"
disabled={$organization.billingPlan === 'tier-0'}>
disabled={$organization.billingPlan === BillingPlan.STARTER}>
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
@@ -132,7 +132,7 @@
<p class="u-color-text-gray u-small">{tierFree.description}</p>
</div>
<div class:u-opacity-50={disabled}>
{#if $organization.billingPlan === 'tier-0'}
{#if $organization.billingPlan === BillingPlan.STARTER}
<Pill disabled>CURRENT PLAN</Pill>
{/if}
</div>
@@ -144,7 +144,7 @@
name="plan"
bind:group={$changeOrganizationTier.billingPlan}
value="tier-1"
disabled={$organization.billingPlan === 'tier-1'}>
disabled={$organization.billingPlan === BillingPlan.PRO}>
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
@@ -158,7 +158,7 @@
</p>
</div>
<div class:u-opacity-50={disabled}>
{#if $organization.billingPlan === 'tier-1'}
{#if $organization.billingPlan === BillingPlan.PRO}
<Pill disabled>CURRENT PLAN</Pill>
{:else}
<Pill>14 DAY FREE TRIAL</Pill>
@@ -1,7 +1,7 @@
<script lang="ts">
import { Box, CreditCardBrandImage } from '$lib/components';
import { CouponInput } from '$lib/components/billing';
import { Pill } from '$lib/elements';
import { BillingPlan } from '$lib/constants';
import { FormList, InputTextarea } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
import { WizardStep } from '$lib/layout';
@@ -11,7 +11,7 @@
import { sdk } from '$lib/stores/sdk';
import { changeOrganizationFinalAction, changeOrganizationTier, isUpgrade } from './store';
const plan = $plansInfo.plans.find((p) => p.$id === $changeOrganizationTier.billingPlan);
const plan = $plansInfo.get($changeOrganizationTier.billingPlan);
const collaboratorPrice = plan?.addons.member?.price ?? 0;
const collaboratorsNumber = $changeOrganizationTier?.collaborators?.length ?? 0;
const totalExpences = plan.price + collaboratorPrice * collaboratorsNumber;
@@ -40,7 +40,7 @@
}
}
$: downgradeToStarter = $changeOrganizationTier.billingPlan === 'tier-0';
$: downgradeToStarter = $changeOrganizationTier.billingPlan === BillingPlan.STARTER;
$: if (!$isUpgrade) {
$changeOrganizationFinalAction = 'Confirm plan change';
}
@@ -72,12 +72,9 @@
<p class="body-text-1 u-bold">Organization name</p>
<div class="u-flex u-gap-8 u-cross-center u-margin-block-start-8">
<p class="text">{$organization.name}</p>
{#if $changeOrganizationTier?.id}
<Pill>{$changeOrganizationTier.id}</Pill>
{/if}
</div>
{#if $changeOrganizationTier.billingPlan !== 'tier-0'}
{#if $changeOrganizationTier.billingPlan !== BillingPlan.STARTER}
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Additional members</p>
<p class="text u-margin-block-start-8">{collaboratorsNumber} members</p>
@@ -104,7 +101,7 @@
bind:coupon
bind:couponData
on:validation={(e) => ($changeOrganizationTier.couponCode = e.detail.code)} />
{#if $changeOrganizationTier.billingPlan !== 'tier-0'}
{#if $changeOrganizationTier.billingPlan !== BillingPlan.STARTER}
<span class="u-flex u-main-space-between">
<p class="text">{plan.name} plan</p>
<p class="text">${plan.price}</p>
@@ -125,7 +122,9 @@
<p class="text">Estimated total</p>
<p class="text">
${couponData?.status === 'active'
? totalExpences - couponData.credits || 0
? totalExpences - couponData.credits >= 0
? totalExpences - couponData.credits
: 0
: totalExpences}
</p>
</span>
@@ -12,12 +12,28 @@
} from '$lib/elements/table';
import { WizardStep } from '$lib/layout';
import { plansInfo } from '$lib/stores/billing';
import { onMount } from 'svelte';
import { changeOrganizationTier } from './store';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
const plan = $plansInfo.plans.find((p) => p.$id === $changeOrganizationTier.billingPlan);
const plan = $plansInfo.get($changeOrganizationTier.billingPlan);
let email: string;
onMount(async () => {
const members = await sdk.forConsole.teams.listMemberships($organization.$id);
if (members.total) {
$changeOrganizationTier.collaborators = members.memberships
.map((m) => {
if (m.userEmail !== $user.email) return m.userEmail;
})
.filter(Boolean);
}
});
function addCollaborator() {
if (!email) return;
if ($changeOrganizationTier.collaborators.includes(email)) return;
@@ -42,10 +58,10 @@
</svelte:fragment>
<Alert type="info">
{#if $changeOrganizationTier.billingPlan === 'tier-2'}
{#if $changeOrganizationTier.billingPlan === BillingPlan.SCALE}
You can add unlimited organization members on the {plan.name} plan at no cost. Each member
added will receive an email invite to your organization on completion.
{:else if $changeOrganizationTier.billingPlan === 'tier-1'}
{:else if $changeOrganizationTier.billingPlan === BillingPlan.PRO}
You can add unlimited organization members on the {plan.name} plan for
<b>${plan.addons.member.price} each per month</b>. Each member added will receive an
email invite to your organization on completion.
@@ -71,7 +87,7 @@
<Table noStyles noMargin>
<TableHeader>
<TableCellHead>Collaborator</TableCellHead>
{#if $changeOrganizationTier.billingPlan === 'tier-1'}
{#if $changeOrganizationTier.billingPlan === BillingPlan.PRO}
<TableCellHead width={80}>Cost</TableCellHead>
{/if}
<TableCellHead width={40} />
@@ -80,7 +96,7 @@
{#each $changeOrganizationTier.collaborators as collaborator}
<TableRow>
<TableCellText title="collaborator">{collaborator}</TableCellText>
{#if $changeOrganizationTier.billingPlan === 'tier-1'}
{#if $changeOrganizationTier.billingPlan === BillingPlan.PRO}
<TableCellText title="cost">15$</TableCellText>
{/if}
<TableCell>
@@ -16,6 +16,7 @@
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { abbreviateNumber } from '$lib/helpers/numbers';
import { formatNum } from '$lib/helpers/string';
import { BillingPlan } from '$lib/constants';
export let excess: {
bandwidth?: number;
@@ -26,13 +27,13 @@
} = null;
export let currentTier: Tier;
const plan = $plansInfo.plans.find((p) => p.$id === currentTier);
const plan = $plansInfo?.get(currentTier);
const collaboratorPrice = plan?.addons.member?.price ?? 0;
</script>
<Alert type="error">
<svelte:fragment slot="title">
{#if currentTier === 'tier-0'}
{#if currentTier === BillingPlan.STARTER}
Your usage exceeds the {plan.name} plan limits
{:else}
Changing your plan now will result in removal of organization members and more
@@ -40,10 +41,10 @@
</svelte:fragment>
{#if excess?.members > 0}
{#if currentTier === 'tier-0'}
{#if currentTier === BillingPlan.STARTER}
The Starter plan has a limit of one organization member. By proceeding, all but the
creator of the organization admin will be removed.
{:else if currentTier === 'tier-1'}
{:else if currentTier === BillingPlan.PRO}
Additional organization members on the Pro plan cost {collaboratorPrice} per member per billing
period. By proceeding, you acknowledge your fees may increase in your next billing period.
{/if}
@@ -56,15 +57,17 @@
{/if}
<svelte:fragment slot="buttons">
{#if currentTier === 'tier-0'}
{#if currentTier === BillingPlan.STARTER}
<Button
text
external
href="https://appwrite.io/docs/advanced/platform/starter#reaching-resource-limits">
Learn more
</Button>
{:else if currentTier === 'tier-1'}
{:else if currentTier === BillingPlan.PRO}
<Button
text
external
href="https://appwrite.io/docs/advanced/platform/pro#reaching-resource-limits">
Learn more
</Button>
@@ -1,5 +1,5 @@
import { BillingPlan } from '$lib/constants';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import type { Address } from '$lib/sdk/billing';
import type { Tier } from '$lib/stores/billing';
import { writable } from 'svelte/store';
@@ -8,11 +8,9 @@ export const isUpgrade = writable<boolean>(false);
export const changeOrganizationFinalAction = writable<string>('Start trial');
export const changeOrganizationTier = writable<{
id?: string;
billingPlan: Tier;
paymentMethodId: string;
billingAddressId: string;
billingAddress?: Address;
billingBudget?: number;
collaborators?: string[];
isOverLimit?: boolean;
@@ -27,20 +25,10 @@ export const changeOrganizationTier = writable<{
feedbackMessage?: string;
couponCode?: string;
}>({
id: null,
billingPlan: 'tier-1',
billingPlan: BillingPlan.PRO,
paymentMethodId: null,
collaborators: [],
isOverLimit: false,
billingAddressId: null,
billingAddress: {
$id: null,
streetAddress: null,
addressLine2: null,
city: null,
state: null,
postalCode: null,
country: null
},
taxId: null
});
+6 -4
View File
@@ -61,10 +61,12 @@
url = `${base}/console${$page.url.search ?? ''}`;
}
}
sdk.forConsole.account.createOAuth2Session('github', url, window.location.origin, [
'read:user',
'user:email'
]);
sdk.forConsole.account.createOAuth2Session(
'github',
window.location.origin + url,
window.location.origin,
['read:user', 'user:email']
);
}
</script>
Binary file not shown.