mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-messaging
This commit is contained in:
@@ -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
|
||||
|
||||
Generated
+4
-4
@@ -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
@@ -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",
|
||||
|
||||
Generated
+7647
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
} else {
|
||||
$wizard.step--;
|
||||
}
|
||||
wizard.setInterceptor(null);
|
||||
trackEvent('wizard_back');
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ?? ''}`);
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
+24
-26
@@ -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">
|
||||
|
||||
+9
-3
@@ -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>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+3
-2
@@ -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
|
||||
});
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user