Merge branch 'main' into feat-pwnd

This commit is contained in:
Matej Bačo
2024-01-07 13:41:26 +01:00
112 changed files with 8741 additions and 851 deletions
+6
View File
@@ -3,8 +3,14 @@ name: Tests
on:
push:
branches: [main]
paths-ignore:
- '**/*.md'
- 'static/**/*'
pull_request:
branches: [main]
paths-ignore:
- '**/*.md'
- 'static/**/*'
env:
VITE_APPWRITE_ENDPOINT: http://appwrite.test/v1
+7647
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -15,7 +15,6 @@
const { input, handleSubmit, completion, isLoading, complete, error } = useCompletion({
api: endpoint + '/console/assistant',
headers: {
'content-type': 'application/json',
'x-appwrite-project': 'console'
},
credentials: 'include'
@@ -9,8 +9,8 @@
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${$page.url.origin}/v1`;
</script>
{#if $actionRequiredInvoices && $actionRequiredInvoices.total && !$page.url.pathname.includes('/console/account')}
<HeaderAlert title="Authorization required">
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !$page.url.pathname.includes('/console/account')}
<HeaderAlert title="Authorization required" type="error">
Please authorize your upcoming payment for {$organization.name}. Your bank requires this
security measure to proceed with payment.
<svelte:fragment slot="buttons">
@@ -1,13 +1,13 @@
<script lang="ts">
// import { base } from '$app/paths';
// import { Button } from '$lib/elements/forms';
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>
+20 -2
View File
@@ -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;
@@ -29,13 +30,16 @@
}
}
});
observer.observe(element, { childList: true });
});
onDestroy(() => {
observer.disconnect();
unmountPaymentElement();
});
$: if (element) {
observer.observe(element, { childList: true });
}
</script>
<RadioBoxes elements={methods} total={methods?.length} variableName="$id" name="payment" bind:group>
@@ -71,3 +75,17 @@
</div>
</FormList>
</RadioBoxes>
<style lang="scss">
.aw-stripe-container {
min-height: 295px;
position: relative;
.loader-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
}
}
</style>
+1 -1
View File
@@ -10,7 +10,7 @@
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
<slot />
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16">
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
<slot name="aside" />
</div>
</div>
+1 -1
View File
@@ -9,7 +9,7 @@
<details class="collapsible-wrapper" {open}>
<!-- svelte-ignore a11y-no-redundant-roles -->
<summary
class="collapsible-button"
class="collapsible-button u-position-relative"
on:keyup={clickOnEnter}
on:click
role="button"
+2 -12
View File
@@ -67,11 +67,11 @@
on:click={handleBLur}
bind:this={backdrop}>
<div
class="modal payment-modal"
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-separate-header={headerDivider}>
<Form isModal {onSubmit} class="payment-form">
<Form isModal {onSubmit}>
<header class="modal-header">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<div class="u-flex u-cross-center u-gap-16">
@@ -151,14 +151,4 @@
background-color: hsl(240 5% 8% / 0.6);
}
}
.payment-modal {
height: 100%;
overflow: hidden;
}
.payment-form {
height: 100%;
overflow: auto;
}
</style>
+2 -2
View File
@@ -115,7 +115,7 @@
<div>
<form on:submit|preventDefault={addFilter}>
<div class="selects u-flex u-gap-8 u-margin-block-start-16">
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
<InputSelect
id="column"
options={$columns.map((c) => ({
@@ -130,7 +130,7 @@
options={operatorsForColumn}
placeholder="Select operator"
bind:value={operatorKey} />
</div>
</ul>
{#if column && operator && !operator?.hideInput}
<div class="u-margin-block-start-8">
{#if column.type === 'integer' || column.type === 'double'}
+3 -3
View File
@@ -26,13 +26,13 @@
}
:global(.theme-dark) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-200));
background: hsl(var(--color-neutral-300));
border: 1px solid hsl(var(--color-neutral-85));
background: hsl(var(--color-neutral-90));
box-shadow: 0px 6px 16px 8px #14141f;
}
:global(.theme-light) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-30));
border: 1px solid hsl(var(--color-neutral-15));
background: hsl(var(--color-neutral-0));
box-shadow: 0px 6px 16px 0px rgba(55, 59, 77, 0.14);
}
+2 -2
View File
@@ -12,7 +12,7 @@
export let backgroundColor: string = null;
export let backgroundColorHover: string = null;
export let tooltipText: string = null;
export let showTooltip = false;
export let tooltipShow = false;
enum Radius {
xsmall = '--border-radius-xsmall',
@@ -31,7 +31,7 @@
style:--card-border-radius={`var(${Radius[borderRadius]})`}
style:--p-card-bg-color-default={backgroundColor}
style:--p-card-bg-color-hover={backgroundColorHover}
use:tooltip={{ content: tooltipText, disabled: !tooltipText || !showTooltip }}>
use:tooltip={{ content: tooltipText, disabled: !tooltipText || !tooltipShow }}>
<div class="u-flex u-gap-8">
<input
class="is-small u-margin-block-start-2"
+1 -1
View File
@@ -10,7 +10,7 @@
<ModalWrapper bind:show let:close {style}>
<div class="grid-1-1 u-width-full-line mk-grid">
<div class="mk-grid-item-1 u-width-full-line">
<div class="mk-grid-item-1 u-width-full-line is-not-mobile">
<slot name="side" />
</div>
<div class="u-padding-32 mk-grid-item-2">
+1 -1
View File
@@ -10,7 +10,7 @@
class:steps-item={!isSub}
class:steps-sub-item={isSub}
class:u-opacity-50={step.disabled}
style={`cursor: ${completed ? 'pointer' : 'default'};`}
style:cursor={completed ? 'pointer' : 'default'}
aria-label={`${completed ? 'done' : current ? 'current' : ''} step`}>
<button
type="button"
+5 -2
View File
@@ -10,10 +10,13 @@
import { isSupportOnline, showSupportModal } from '../../routes/console/wizard/support/store';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
export let show = false;
$: isPaid = $organization?.billingPlan === 'tier-1' || $organization?.billingPlan === 'tier-2';
$: isPaid =
$organization?.billingPlan === BillingPlan.PRO ||
$organization?.billingPlan === BillingPlan.SCALE;
</script>
{#if isCloud}
@@ -39,7 +42,7 @@
</p>
{/if}
</div>
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button fullWidth href="https://appwrite.io/pricing" external>
<span class="text">Get Premium support</span>
</Button>
+7
View File
@@ -8,6 +8,7 @@ export enum Dependencies {
ADDRESS = 'dependency:address',
PAYMENT_METHODS = 'dependency:paymentMethods',
ORGANIZATION = 'dependency:organization',
MEMBERS = 'dependency:members',
PROJECT = 'dependency:project',
PROJECT_VARIABLES = 'dependency:project_variables',
PROJECT_INSTALLATIONS = 'dependency:project_installations',
@@ -616,3 +617,9 @@ export const limitRates = {
}
]
};
export enum BillingPlan {
STARTER = 'tier-0',
PRO = 'tier-1',
SCALE = 'tier-2'
}
+8 -1
View File
@@ -23,6 +23,7 @@
let classes: string = undefined;
export { classes as class };
export let actions: MultiActionArray = [];
export let submissionLoader = false;
const isSubmitting = hasContext('form')
? getContext<FormContext>('form').isSubmitting
@@ -77,6 +78,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}
+4 -7
View File
@@ -31,15 +31,12 @@
$: if ($value) {
data = $value;
}
$: console.log(data);
</script>
{#if label}
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
<!-- <span class="label" use:label>Hotel Nights</span> -->
{/if}
<div class="u-flex u-gap-4">
{#if showDates}
@@ -139,10 +136,10 @@
border-radius: 100%;
user-select: none;
[aria-disabled='true'] {
pointer-events: none;
opacity: 0.4;
}
// [aria-disabled='true'] {
// pointer-events: none;
// opacity: 0.4;
// }
}
.dt-button {
aspect-ratio: 1/1;
+8 -2
View File
@@ -7,7 +7,7 @@
export let showLabel = true;
export let optionalText: string | undefined = undefined;
export let id: string;
export let value = '';
export let value: string;
export let required = false;
export let nullable = false;
export let disabled = false;
@@ -35,6 +35,11 @@
error = element.validationMessage;
}
function handleInput(event: Event) {
const { value: currentValue } = event.currentTarget as HTMLInputElement;
value = currentValue || null;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
@@ -64,12 +69,13 @@
{disabled}
{readonly}
{required}
{value}
step=".001"
autocomplete={autocomplete ? 'on' : 'off'}
type="datetime-local"
class="input-text"
bind:value
bind:this={element}
on:input={handleInput}
on:invalid={handleInvalid}
style:--amount-of-buttons={isNullable ? 2.75 : 1}
style:--button-size={isNullable ? '2rem' : '1rem'} />
+1 -1
View File
@@ -8,7 +8,7 @@
let element: HTMLInputElement;
let icon = 'info';
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`;
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._\-]*$`;
onMount(() => {
if (element && autofocus) {
+5 -1
View File
@@ -6,13 +6,17 @@
let className = '';
export { className as class };
export let style = '';
export let right = false;
</script>
<div
style={`${style} ${width ? `--p-col-width:${width?.toString()}` : ''}`}
{style}
style:--p-col-width={width?.toString() ?? ''}
class="table-col {className}"
class:u-overflow-visible={showOverflow}
class:is-only-desktop={onlyDesktop}
class:u-flex={right}
class:u-main-end={right}
data-title={title}
role="cell">
<slot />
+2 -1
View File
@@ -9,7 +9,8 @@
class="table is-selected-columns-mobile"
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
style={`${style} ${transparent ? '--p-table-bg-color: var(--transparent)' : ''}`}
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}
role="table"
data-private>
<slot />
+2 -1
View File
@@ -45,7 +45,8 @@
<table
class="table"
class:is-sticky-scroll={isSticky && isOverflowing}
style={`${style} ${transparent ? '--p-table-bg-color: var(--transparent)' : ''}`}>
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}>
<slot />
</table>
</div>
+6 -2
View File
@@ -21,6 +21,8 @@
import { tierToPlan, type PlanServices } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
export let logs: Models.LogList;
export let offset = 0;
@@ -50,8 +52,10 @@
Logs are retained in rolling {hoursToDays(limit)} intervals with the
{tierToPlan($organization.billingPlan).name}
plan.
<button class="link" type="button" on:click|preventDefault={upgradeMethod}
>Upgrade</button> to increase your log retention for a longer period.
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button link on:click={upgradeMethod}>Upgrade</Button> to increase your log
retention for a longer period.
{/if}
</p>
</svelte:fragment>
{#each logs.logs as log}
+2 -1
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
export let title: string;
export let tooltipContent =
$organization.billingPlan === 'tier-0'
$organization.billingPlan === BillingPlan.STARTER
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization.billingPlan).name
+7 -6
View File
@@ -17,6 +17,7 @@
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { ContainerButton } from '.';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
export let isFlex = true;
export let title: string;
@@ -34,7 +35,7 @@
let showDropdown = false;
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits;
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {};
const limitedServices = [
{ name: 'bandwidth', value: bandwidth },
{ name: 'documents', value: documents },
@@ -52,7 +53,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;
@@ -77,7 +78,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.
@@ -101,7 +102,7 @@
{/if}
<header class:u-flex={isFlex} class="u-gap-12 common-section u-main-space-between u-flex-wrap">
<div class="u-flex u-cross-child-center u-gap-16 u-flex-wrap">
<div class="u-flex u-cross-child-center u-cross-center u-gap-16 u-flex-wrap">
<Heading tag={titleTag} size={titleSize}>{title}</Heading>
{#if isCloud && isLimited}
<DropList bind:show={showDropdown} width="16">
@@ -121,7 +122,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()}.
@@ -139,7 +140,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}
+2 -2
View File
@@ -27,6 +27,7 @@
import { Pill } from '$lib/elements';
import { showExcess } from '$routes/console/organization-[organization]/store';
import { readOnly } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
let showDropdown = false;
let showSupport = false;
@@ -117,10 +118,9 @@
<div class="main-header-end">
<nav class="u-flex is-only-desktop u-cross-center">
{#if isCloud && $organization?.billingPlan === 'tier-0' && !$page.url.pathname.startsWith('/console/account')}
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
<Button
disabled={$organization?.markedForDeletion}
secondary
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
Upgrade
</Button>
+10 -7
View File
@@ -19,6 +19,8 @@
import { getServiceLimit, tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { app } from '$lib/stores/app';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
@@ -310,13 +312,14 @@
<Alert>
Logs are retained in rolling {hoursToDays(limit)} intervals
with the {tier} plan.
<button
class="link"
type="button"
on:click|preventDefault={() =>
wizard.start(ChangeOrganizationTierCloud)}
>Upgrade</button> to increase your log retention for
a longer period.
{#if $organization.billingPlan === BillingPlan.STARTER}
<Button
link
on:click={() =>
wizard.start(ChangeOrganizationTierCloud)}
>Upgrade</Button> to increase your log retention
for a longer period.
{/if}
</Alert>
{/if}
<Code withCopy noMargin code={execution.logs} language="sh" />
+2 -1
View File
@@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { BillingPlan } from '$lib/constants';
import { isMac } from '$lib/helpers/platform';
import { slide } from '$lib/helpers/transition';
import { organization } from '$lib/stores/organization';
@@ -180,7 +181,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"
+17 -17
View File
@@ -6,8 +6,8 @@
import { app } from '$lib/stores/app';
import { user } from '$lib/stores/user';
export let imgLight = LoginLight;
export let imgDark = LoginDark;
export const imgLight = LoginLight;
export const imgDark = LoginDark;
</script>
<main class="grid-1-1 is-full-page" id="main">
@@ -384,18 +384,18 @@
padding-inline-end: 1rem;
}
.tag-line {
font-family: 'Aeonik Pro';
font-size: 4rem;
font-style: normal;
font-weight: 400;
line-height: 100%; /* 80px */
letter-spacing: -1.6px;
backdrop-filter: blur(0.5 rem);
.underscore {
-webkit-text-fill-color: #f02e65;
}
}
// .tag-line {
// font-family: 'Aeonik Pro';
// font-size: 4rem;
// font-style: normal;
// font-weight: 400;
// line-height: 100%; /* 80px */
// letter-spacing: -1.6px;
// backdrop-filter: blur(0.5 rem);
// .underscore {
// -webkit-text-fill-color: #f02e65;
// }
// }
}
/* for smaller screens */
@@ -418,9 +418,9 @@
padding-inline-start: 5.625rem;
padding-inline-end: 5rem;
}
.tag-line {
font-size: 5rem;
}
// .tag-line {
// font-size: 5rem;
// }
}
}
</style>
+1
View File
@@ -122,6 +122,7 @@
} else {
$wizard.step--;
}
wizard.setInterceptor(null);
trackEvent('wizard_back');
}
+8 -1
View File
@@ -8,10 +8,17 @@
function handleSubmit() {
dispatch('exit');
show = false;
}
</script>
<Modal title="Exit Process" bind:show onSubmit={handleSubmit} icon="exclamation" state="warning">
<Modal
title="Exit Process"
bind:show
onSubmit={handleSubmit}
icon="exclamation"
state="warning"
headerDivider={false}>
<p>
Are you sure you want to exit from <slot />? All data will be deleted. This action is
irreversible.
+19 -13
View File
@@ -185,15 +185,15 @@ export type AggregationList = {
};
export type AllowedRegions =
| 'eu-de'
| 'us-nyc'
| 'us-sfo'
| 'ap-in'
| 'eu-gb'
| 'eu-nl'
| 'ap-sg'
| 'ap-ca'
| 'ap-au'
| 'fra'
| 'nyc'
| 'sfo'
| 'blr'
| 'lon'
| 'ams'
| 'sgp'
| 'tor'
| 'syd'
| 'default'; //TODO: remove after migration
export type Region = {
@@ -265,6 +265,8 @@ export type PlansInfo = {
total: number;
};
export type PlansMap = Map<Tier, Plan>;
export class Billing {
client: Client;
@@ -276,14 +278,16 @@ export class Billing {
organizationId: string,
name: string,
billingPlan: string,
paymentMethodId: string
paymentMethodId: string,
billingAddressId: string
): Promise<Organization> {
const path = `/organizations`;
const params = {
organizationId,
name,
billingPlan,
paymentMethodId
paymentMethodId,
billingAddressId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
@@ -331,13 +335,15 @@ export class Billing {
async updatePlan(
organizationId: string,
billingPlan: string,
paymentMethodId: string
paymentMethodId: string,
billingAddressId: string
): Promise<Organization> {
const path = `/organizations/${organizationId}/plan`;
const params = {
organizationId,
billingPlan,
paymentMethodId
paymentMethodId,
billingAddressId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
+29 -44
View File
@@ -2,43 +2,36 @@ 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 { diffDays } 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 { showPostReleaseModal } from '$routes/console/store';
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 type ReadOnlyData = {
// bandwidth: boolean;
// documents: boolean;
// executions: boolean;
// storage: boolean;
// users: boolean;
// };
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;
@@ -71,8 +64,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];
}
@@ -125,7 +118,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':
@@ -165,58 +158,45 @@ 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);
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')
]);
actionRequiredInvoices.set(invoices);
if (get(actionRequiredInvoices) && get(actionRequiredInvoices)?.invoices?.length > 0) {
if (invoices?.invoices?.length > 0) {
headerAlert.add({
id: 'paymentAuthRequired',
component: PaymentAuthRequired,
@@ -224,6 +204,9 @@ export async function checkPaymentAuthorizationRequired(org: Organization) {
importance: 8
});
}
activeHeaderAlert.set(headerAlert.get());
actionRequiredInvoices.set(invoices);
}
export async function paymentExpired(org: Organization) {
@@ -288,11 +271,13 @@ export async function checkForFreeOrgOverflow(orgs: Models.TeamList<Record<strin
show: true,
importance: 10
});
activeHeaderAlert.set(headerAlert.get());
}
}
export async function checkForPostReleaseProModal(orgs: Models.TeamList<Record<string, unknown>>) {
if (!orgs?.teams?.length) return;
if (orgs.total > orgs.teams.length) return; // if the total is greater that the free orgs it means that there are pro orgs
const modalTime = localStorage.getItem('postReleaseProModal');
const now = Date.now();
// show the modal if it was never shown
+1 -1
View File
@@ -25,7 +25,7 @@ function createHeaderAlertStore() {
if (n.components.some((c) => c.id === component.id)) return n;
else {
n.components.push(component);
n.components = n.components;
n.components = [...n.components];
return n;
}
});
+1 -1
View File
@@ -14,7 +14,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
billingCurrentInvoiceDate: string;
billingNextInvoiceDate: string;
billingTrialStartDate?: string;
billingTrialEndDate?: string;
billingStartDate?: string;
billingTrialDays?: number;
billingAddressId?: string;
amount: number;
+55 -29
View File
@@ -4,6 +4,7 @@ import { app } from './app';
import { get, writable } from 'svelte/store';
import type { PaymentMethodData } from '$lib/sdk/billing';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from './notifications';
export const stripe = writable<Stripe>();
let paymentMethod: PaymentMethodData;
@@ -14,36 +15,35 @@ let paymentElement: StripeElement;
export const isStripeInitialized = writable(false);
export async function initializeStripe() {
console.log('Initializing Stripe');
if (!get(stripe)) return;
isStripeInitialized.set(true);
try {
const methods = await sdk.forConsole.billing.listPaymentMethods();
clientSecret = methods.paymentMethods[0]?.clientSecret;
// If there is no payment method, create an empty one and get the client secret
if (!clientSecret) {
paymentMethod = await sdk.forConsole.billing.createPaymentMethod();
clientSecret = paymentMethod.clientSecret;
}
console.log('client secret', clientSecret);
// Set up the options for the stripe elements
const options = {
clientSecret: clientSecret,
appearance: get(app).themeInUse === 'dark' ? apperanceDark : apperanceLight
};
// Set up Elements and then create form
elements = get(stripe).elements(options);
paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
} catch (e) {
console.log(e);
throw e;
const methods = await sdk.forConsole.billing.listPaymentMethods();
clientSecret = methods.paymentMethods[0]?.clientSecret;
// If there is no payment method, create an empty one and get the client secret
if (!clientSecret) {
paymentMethod = await sdk.forConsole.billing.createPaymentMethod();
clientSecret = paymentMethod.clientSecret;
}
// Set up the options for the stripe elements
const options = {
clientSecret: clientSecret,
appearance: get(app).themeInUse === 'dark' ? apperanceDark : apperanceLight
};
// Set up Elements and then create form
elements = get(stripe).elements(options);
paymentElement = elements.create('payment');
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 {
@@ -62,7 +62,9 @@ export async function submitStripeCard(name: string, urlRoute?: string) {
elements,
clientSecret,
confirmParams: {
return_url: `${baseUrl}${urlRoute ?? 'organization/billing?invoiceId'}`,
return_url: `${baseUrl}${
urlRoute ?? `organization/billing?clientSecret=${clientSecret}`
}`,
payment_method_data: {
billing_details: {
name
@@ -75,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') {
@@ -91,16 +93,40 @@ export async function submitStripeCard(name: string, urlRoute?: string) {
} else {
const e = new Error('Something went wrong');
trackError(e, Submit.PaymentMethodCreate);
console.log('setupIntent failed');
throw e;
throw e.message;
}
} catch (e) {
trackError(e, Submit.PaymentMethodCreate);
console.log(e);
throw e;
}
}
export async function confirmPayment(orgId: string, clientSecret: string, paymentMethodId: string) {
try {
const url = `${window.location.origin}/console/organization-${orgId}/billing`;
const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentMethodId);
const { error } = await get(stripe).confirmPayment({
clientSecret: clientSecret,
confirmParams: {
return_url: url,
payment_method: paymentMethod.providerMethodId
}
});
if (error) {
throw error.message;
}
} catch (e) {
addNotification({
title: 'Error',
message:
'There was an error processing your payment, try again later. If the problem persists, please contact support.',
type: 'error'
});
}
}
const apperanceLight = {
variables: {
colorPrimary: '#606a7b',
+6 -2
View File
@@ -31,14 +31,18 @@ function createWizardStore() {
return {
subscribe,
set,
start: (component: typeof SvelteComponent<unknown>, media: string = null) =>
start: (
component: typeof SvelteComponent<unknown>,
media: string = null,
step: number = 1
) =>
update((n) => {
n.show = true;
n.component = component;
n.interceptor = null;
n.interceptorNotificationEnabled = true;
n.media = media;
n.step = 1;
n.step = step;
n.cover = null;
n.nextDisabled = false;
n.finalAction = null;
@@ -87,13 +87,13 @@
]}
value={null} />
</div>
<div class="u-width-full-line">
<ul class="u-width-full-line">
<InputSearch placeholder="Search repositories" disabled />
</div>
</ul>
</div>
{:then installations}
<div class="u-flex u-gap-16">
<div class="u-width-full-line">
<ul class="u-width-full-line">
<InputSelect
id="installation"
label="Select installation"
@@ -111,10 +111,10 @@
);
}}
bind:value={selectedInstallation} />
</div>
<div class="u-width-full-line">
</ul>
<ul class="u-width-full-line">
<InputSearch placeholder="Search repositories" bind:value={search} />
</div>
</ul>
</div>
{/await}
<p class="text u-margin-block-start-16">
@@ -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')
);
@@ -98,7 +100,12 @@
<Button on:click={wizard.hide}>Go to console</Button>
</div>
</Card>
<img class="hoodie-image" src={Hoodie} alt="" srcset="" />
<img
class="hoodie-image"
src={Hoodie}
alt=""
srcset=""
style="aspect-ratio: 1.56 / 1;" />
<div class="cbc-confetti" transition:fade>
<Confetti
x={[-1.75, 1.85]}
@@ -25,10 +25,10 @@
async function handleUpgrade() {
if (selectedOrg) {
await goto(`${base}/console/organization/${selectedOrg}/billing`);
await goto(`${base}/console/organization-${selectedOrg}/billing`);
} else {
const freeOrg = orgs.teams.find((o) => o.billingPlan === 'tier-0');
await goto(`${base}/console/organization/${freeOrg.$id}/billing`);
await goto(`${base}/console/organization-${freeOrg.$id}/billing`);
}
show = false;
wizard.start(ChangeOrganizationTierCloud);
@@ -37,11 +37,11 @@
});
}
$: isNewUser = new Date($user.$createdAt) > new Date(2023, 11, 18);
//user creation date after the 18th of December 2023
$: isNewUser = new Date($user.$createdAt) > new Date(2023, 11, 19);
//user creation date after the 19th of December 2023
$: event = isNewUser ? 'billing_new_user_modal' : 'billing_release_modal';
$: freeOrgs = orgs?.teams.filter((o) => o.billingPlan === 'tier-0');
$: options = freeOrgs.map((o) => ({ label: o.name, value: o.$id }));
$: freeOrgs = orgs?.teams?.filter((o) => o.billingPlan === 'tier-0');
$: options = freeOrgs?.map((o) => ({ label: o.name, value: o.$id }));
</script>
<ModalSideCol bind:show title="Claim your Pro credit today" style="max-width: min(53rem,95%)">
@@ -86,7 +86,7 @@
}}>Learn more on our pricing page.</Button>
</p>
{#if freeOrgs.length > 1 && options.length}
{#if freeOrgs?.length > 1 && options?.length}
<FormList class="u-margin-block-start-24">
<InputSelect
label="Organization to upgrade"
+7 -14
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { INTERVAL } from '$lib/constants';
import { BillingPlan, INTERVAL } from '$lib/constants';
import { Logs } from '$lib/layout';
import Footer from '$lib/layout/footer.svelte';
import Header from '$lib/layout/header.svelte';
@@ -18,7 +18,6 @@
checkForUsageLimit,
checkPaymentAuthorizationRequired,
calculateTrialDay,
checkForTrialEnding,
paymentExpired,
checkForFreeOrgOverflow,
checkForPostReleaseProModal,
@@ -42,7 +41,7 @@
import ExcesLimitModal from './organization-[organization]/excesLimitModal.svelte';
import { showExcess } from './organization-[organization]/store';
import UsageRates from './wizard/cloudOrganization/usageRates.svelte';
import { consoleVariables, showPostReleaseModal } from './store';
import { activeHeaderAlert, consoleVariables, showPostReleaseModal } from './store';
import { Query } from '@appwrite.io/console';
import { headerAlert } from '$lib/stores/headerAlert';
import PostReleaseModal from './(billing-modal)/postReleaseModal.svelte';
@@ -242,22 +241,17 @@
}
]);
let isOpen = false;
let selectedHeaderAlert = headerAlert.get();
onMount(async () => {
loading.set(false);
if (isCloud) {
if (!$page.url.pathname.includes('/console/onboarding')) {
}
try {
const orgs = await sdk.forConsole.teams.list([
Query.equal('billingPlan', 'tier-0')
Query.equal('billingPlan', BillingPlan.STARTER)
]);
checkForPostReleaseProModal(orgs);
checkForFreeOrgOverflow(orgs);
} catch (error) {
console.log(error);
}
}
@@ -287,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);
}
@@ -307,7 +300,7 @@
$registerSearchers(orgSearcher, projectsSearcher);
afterUpdate(() => {
selectedHeaderAlert = headerAlert.get();
$activeHeaderAlert = headerAlert.get();
});
</script>
@@ -321,8 +314,8 @@
!$page.url.pathname.includes('/console/card') &&
!$page.url.pathname.includes('/console/onboarding')}>
<svelte:fragment slot="alert">
{#if selectedHeaderAlert?.show}
<svelte:component this={selectedHeaderAlert.component} />
{#if $activeHeaderAlert?.show}
<svelte:component this={$activeHeaderAlert.component} />
{/if}
</svelte:fragment>
<Header slot="header" />
+8 -2
View File
@@ -1,4 +1,6 @@
import { Dependencies } from '$lib/constants';
import type { Plan } from '$lib/sdk/billing';
import type { Tier } from '$lib/stores/billing';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
@@ -18,9 +20,13 @@ export const load: LayoutLoad = async ({ fetch, depends }) => {
const [data, variables] = await Promise.all([versionPromise, variablesPromise]);
let plansInfo = null;
let plansInfo = new Map<Tier, Plan>();
if (isCloud) {
plansInfo = await sdk.forConsole.billing.getPlansInfo();
const plansArray = await sdk.forConsole.billing.getPlansInfo();
plansInfo = plansArray.plans.reduce((map, plan) => {
map.set(plan.$id as Tier, plan);
return map;
}, new Map<Tier, Plan>());
}
return {
+3 -1
View File
@@ -7,7 +7,9 @@ export const load: PageLoad = async ({ parent, url }) => {
if (organizations.total) {
const teamId = account.prefs.organization ?? organizations.teams[0].$id;
throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
if (!teamId) {
throw redirect(303, `${base}/console/account/organizations${url.search ?? ''}`);
} else throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
} else {
throw redirect(303, `${base}/console/onboarding${url.search ?? ''}`);
}
@@ -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}
@@ -4,7 +4,6 @@
import PaymentMethods from './paymentMethods.svelte';
import BillingAddress from './billingAddress.svelte';
// export let data;
let showPayment = false;
</script>
@@ -47,7 +47,7 @@
address,
city,
state,
zip,
zip ? zip : undefined,
address2 ? address2 : undefined
);
trackEvent(Submit.BillingAddressCreate);
@@ -22,6 +22,10 @@
onMount(async () => {
const countryList = await sdk.forProject.locale.listCountries();
const locale = await sdk.forProject.locale.get();
if (locale.countryCode) {
selectedAddress.country = locale.countryCode;
}
options = countryList.countries.map((country) => {
return {
value: country.code,
@@ -38,7 +42,7 @@
selectedAddress.streetAddress,
selectedAddress.city,
selectedAddress.state,
selectedAddress.postalCode,
selectedAddress.postalCode ? selectedAddress.postalCode : undefined,
selectedAddress.addressLine2 ? selectedAddress.addressLine2 : undefined
);
await invalidate(Dependencies.ADDRESS);
@@ -1,5 +1,4 @@
<script lang="ts">
// import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Alert, Modal } from '$lib/components';
import { Button, FormItem, FormList, InputNumber, InputSelect } from '$lib/elements/forms';
import { invalidate } from '$app/navigation';
@@ -55,14 +55,16 @@
}
}
});
observer.observe(element, { childList: true });
});
onDestroy(() => {
observer.disconnect();
document.documentElement.classList.remove('u-overflow-hidden');
});
$: if (element) {
observer.observe(element, { childList: true });
}
</script>
<FakeModal bind:show title="Add payment method" bind:error onSubmit={handleSubmit}>
@@ -90,7 +92,7 @@
</svelte:fragment>
</FakeModal>
<style lang="scss" global>
<style lang="scss">
.aw-stripe-container {
min-height: 295px;
position: relative;
@@ -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,33 +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.addressLine2
? $changeOrganizationTier.billingAddress.addressLine2
: undefined
);
await sdk.forConsole.billing.setBillingAddress(org.$id, response.$id);
}
//Add coupon
if ($changeOrganizationTier.couponCode) {
await sdk.forConsole.billing.addCredit(
@@ -166,7 +143,6 @@
});
}
trackEvent($isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade, {
customId: !!$changeOrganizationTier.id,
plan: tierToPlan($changeOrganizationTier.billingPlan)?.name
});
wizard.hide();
@@ -174,7 +150,7 @@
} catch (e) {
addNotification({
type: 'error',
message: e.mesage
message: e.message
});
trackError(
e,
@@ -186,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
};
@@ -238,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,35 +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.addressLine2
? $createOrganization.billingAddress.addressLine2
: undefined
);
await sdk.forConsole.billing.setBillingAddress(org.$id, response.$id);
}
//Add budget
if ($createOrganization?.billingBudget) {
@@ -86,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}`);
@@ -93,20 +89,15 @@
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) {
addNotification({
type: 'error',
message: e.mesage
message: e.message
});
trackError(e, Submit.OrganizationCreate);
}
@@ -115,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
};
});
@@ -160,4 +142,5 @@
title="Create organization"
steps={$createOrgSteps}
finalAction={$createOrganizationFinalAction}
on:exit={onFinish} />
on:exit={onFinish}
confirmExit />
+107 -44
View File
@@ -2,11 +2,11 @@
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Card } from '$lib/components';
import { Card, Heading } from '$lib/components';
import CustomId from '$lib/components/customId.svelte';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button, Form, InputText } from '$lib/elements/forms';
import { Button, Form, InputSelect, InputText } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import { Container } from '$lib/layout';
import { addNotification } from '$lib/stores/notifications';
@@ -16,10 +16,18 @@
import { ID } from '@appwrite.io/console';
import { onMount } from 'svelte';
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
import { tierToPlan, type Tier } from '$lib/stores/billing';
import { createOrganization } from '../wizard/cloudOrganization/store';
let name: string;
let id: string;
let showCustomId = false;
let plan: Tier;
const options = [
{ value: BillingPlan.STARTER, label: 'Starter - $0/month' },
{ value: BillingPlan.PRO, label: 'Pro - $15/month + add-ons' }
];
onMount(() => {
if (isCloud) {
@@ -32,65 +40,120 @@
}
});
async function createProject() {
try {
const org = await createOrganization();
const project = await sdk.forConsole.projects.create(
id ?? ID.unique(),
name,
org.$id,
isCloud ? 'eu-de' : 'default'
);
await invalidate(Dependencies.ACCOUNT);
goto(`/console/project-${project.$id}`);
trackEvent(Submit.ProjectCreate, {
customId: !!id,
teamId: org.$id
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.ProjectCreate);
}
}
async function createOrganization() {
async function handleSubmit() {
const orgName = name?.length ? name : 'Personal Projects';
if (isCloud) {
return await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal Projects',
'tier-0',
null
);
} else return await sdk.forConsole.teams.create(ID.unique(), 'Personal Projects');
if (plan === BillingPlan.STARTER) {
try {
const org = await sdk.forConsole.billing.createOrganization(
id ?? ID.unique(),
orgName,
plan,
null,
null
);
trackEvent(Submit.OrganizationCreate, {
customId: !!id,
plan: tierToPlan(plan)?.name
});
await invalidate(Dependencies.ACCOUNT);
await goto(`/console/organization-${org.$id}`);
addNotification({
message: `${orgName} organization successfully created`,
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.OrganizationCreate);
}
} else {
wizard.start(CreateOrganizationCloud, null, 2);
$createOrganization.name = orgName;
$createOrganization.billingPlan = plan;
$createOrganization.id = id;
}
} else {
try {
const org = await sdk.forConsole.teams.create(id ?? ID.unique(), orgName);
await invalidate(Dependencies.ACCOUNT);
await goto(`/console/organization-${org.$id}`);
addNotification({
message: `${orgName} organization successfully created`,
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.OrganizationCreate);
}
}
}
</script>
<Container overlapCover size="large">
<Card>
<Form onSubmit={createProject}>
<FormList>
<Heading size="4" tag="h2">Create a new organization</Heading>
<Form onSubmit={handleSubmit}>
<FormList gap={16}>
<InputText
id="name"
label="Project name"
placeholder="First Appwrite Project"
required
label="Name"
placeholder="Organization name"
hideRequired
bind:value={name} />
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
Project ID
Organization ID
</span>
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
<CustomId bind:show={showCustomId} name="Organization" isProject bind:id />
{/if}
<Button fullWidth submit disabled={name === ''} event="create_project">
Create project
{#if isCloud}
<div class="u-margin-block-start-8">
<h3><b>Plan</b></h3>
<p>
For more details on our plans, visit our <Button
link
external
href="https://appwrite.io/pricing">pricing page</Button
>.
</p>
<FormList class="u-margin-block-start-8">
<InputSelect
placeholder="Select a plan"
label="Select plan"
showLabel={false}
id="plan"
required
hideRequired
{options}
bind:value={plan} />
</FormList>
</div>
{/if}
<Button
fullWidth
submit
disabled={isCloud && !plan}
event="create_organization"
submissionLoader
let:isSubmitting>
{#if isSubmitting}
Creating your first organization
{:else}
Get started
{/if}
</Button>
</FormList>
</Form>
+5 -2
View File
@@ -1,7 +1,10 @@
<script lang="ts">
import { Cover, CoverTitle } from '$lib/layout';
import { Heading } from '$lib/components';
import { Cover } from '$lib/layout';
</script>
<Cover size="large">
<CoverTitle>Let's create your first project</CoverTitle>
<div class="u-flex u-cross-center u-main-center u-width-full-line">
<Heading size="1" tag="h1" trimmed={false}>Welcome to Appwrite</Heading>
</div>
</Cover>
@@ -9,9 +9,11 @@ import Header from './header.svelte';
import { headerAlert } from '$lib/stores/headerAlert';
import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte';
import { get } from 'svelte/store';
import { preferences } from '$lib/stores/preferences';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.ORGANIZATION);
depends(Dependencies.MEMBERS);
depends(Dependencies.PAYMENT_METHODS);
if (isCloud) {
@@ -28,6 +30,10 @@ export const load: LayoutLoad = async ({ params, depends }) => {
}
try {
const prefs = await sdk.forConsole.account.getPrefs();
const newPrefs = { ...prefs, organization: params.organization };
sdk.forConsole.account.updatePrefs(newPrefs);
preferences.loadTeamPrefs(params.organization);
return {
header: Header,
breadcrumbs: Breadcrumbs,
@@ -35,6 +41,9 @@ export const load: LayoutLoad = async ({ params, depends }) => {
members: await sdk.forConsole.teams.listMemberships(params.organization)
};
} catch (e) {
const prefs = await sdk.forConsole.account.getPrefs();
const newPrefs = { ...prefs, organization: null };
sdk.forConsole.account.updatePrefs(newPrefs);
localStorage.removeItem('organization');
throw error(e.code, e.message);
}
@@ -31,6 +31,7 @@
import type { RegionList } from '$lib/sdk/billing';
import { onMount } from 'svelte';
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
import { organization } from '$lib/stores/organization';
export let data;
@@ -136,93 +137,96 @@
}
</script>
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Projects</Heading>
{#if $organization?.$id}
<Container>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<Heading tag="h2" size="5">Projects</Heading>
<DropList bind:show={showDropdown} placement="bottom-end">
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create project</span>
</Button>
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
<DropList bind:show={showDropdown} placement="bottom-end">
<Button
on:click={handleCreateProject}
event="create_project"
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create project</span>
</Button>
<svelte:fragment slot="list">
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
<DropListItem on:click={importProject}>
<div class="u-flex u-gap-8 u-cross-center">
Import project <span class="tag eyebrow-heading-3">Experimental</span>
</div>
</DropListItem>
</svelte:fragment>
</DropList>
</div>
{#if data.projects.total}
<CardContainer
total={data.projects.total}
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
<li>
<GridItem1 href={`${base}/console/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
</p>
{/if}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
{#each platforms as platform, i}
{#if i < 3}
{#if data.projects.total}
<CardContainer
total={data.projects.total}
offset={data.offset}
on:click={handleCreateProject}>
{#each data.projects.projects as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
<li>
<GridItem1 href={`${base}/console/project-${project.$id}`}>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">
{project.name}
</svelte:fragment>
{#if allServiceDisabled(project)}
<p>
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
</p>
{/if}
{#each platforms as platform, i}
{#if i < 3}
<Pill>
<span class={`icon-${platform.icon}`} aria-hidden="true" />
{platform.name}
</Pill>
{/if}
{/each}
{#if platforms?.length > 3}
<Pill>
<span class={`icon-${platform.icon}`} aria-hidden="true" />
{platform.name}
+{project.platforms.length - 3}
</Pill>
{/if}
{/each}
{#if platforms?.length > 3}
<Pill>
+{project.platforms.length - 3}
</Pill>
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray">
{region.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
</li>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray u-medium u-line-height-2">
{region?.name}
</span>
{/if}
</svelte:fragment>
</GridItem1>
</li>
{/each}
<svelte:fragment slot="empty">
<p>Create a new project</p>
</svelte:fragment>
</CardContainer>
{:else}
<Empty
single
on:click={handleCreateProject}
target="project"
href="https://appwrite.io/docs/quick-starts"></Empty>
{/if}
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
</Container>
<PaginationWithLimit
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
</Container>
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
{/if}
@@ -17,7 +17,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
projects: await sdk.forConsole.projects.list([
Query.offset(offset),
Query.equal('teamId', params.organization),
Query.limit(CARD_LIMIT),
Query.limit(limit),
Query.orderDesc('')
])
};
@@ -14,12 +14,12 @@
import type { PaymentMethodData } from '$lib/sdk/billing';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { initializeStripe, isStripeInitialized, stripe } from '$lib/stores/stripe';
import { confirmPayment } from '$lib/stores/stripe';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
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
@@ -36,45 +36,27 @@
) {
wizard.start(ChangeOrganizationTierCloud);
}
if (
($page.url.searchParams.has('invoice') &&
$page.url.searchParams.has('type') &&
$page.url.searchParams.get('type') === 'confirmation') ||
$page.url.searchParams.has('clientSecret')
) {
if (!$isStripeInitialized) {
await initializeStripe();
}
try {
const invoiceId = $page.url.searchParams.get('invoice');
const invoice = await sdk.forConsole.billing.getInvoice(
$page.params.organization,
invoiceId
);
const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(
$organization.paymentMethodId
);
const url = `${window.location.origin}/console/organization-${$organization.$id}/billing`;
const { error } = await $stripe.confirmPayment({
clientSecret: invoice.clientSecret,
confirmParams: {
return_url: url,
payment_method: paymentMethod.providerMethodId
}
});
if (error) {
console.log('error', error);
throw new Error();
}
} catch (e) {
addNotification({
title: 'Error',
message:
'There was an error processing your payment, try again later. If the problem persists, please contact support.',
type: 'error'
});
}
if (
$page.url.searchParams.has('invoice') &&
$page.url.searchParams.has('type') &&
$page.url.searchParams.get('type') === 'confirmation'
) {
const invoiceId = $page.url.searchParams.get('invoice');
const invoice = await sdk.forConsole.billing.getInvoice(
$page.params.organization,
invoiceId
);
await confirmPayment(
$organization.$id,
invoice.clientSecret,
$organization.paymentMethodId
);
}
if ($page.url.searchParams.has('clientSecret')) {
const clientSecret = $page.url.searchParams.get('clientSecret');
await confirmPayment($organization.$id, clientSecret, $organization.paymentMethodId);
}
});
</script>
@@ -114,7 +96,7 @@
<BillingAddress />
<TaxId />
<BudgetCap />
{#if $organization?.billingPlan !== 'tier-0' && !!$organization?.billingBudget}
{#if $organization?.billingPlan !== BillingPlan.STARTER && !!$organization?.billingBudget}
<BudgetAlert />
{/if}
<AvailableCredit />
@@ -38,7 +38,7 @@
} catch (e) {
addNotification({
type: 'error',
message: e.mesage
message: e.message
});
trackError(e, Submit.CreditRedeem);
}
@@ -27,9 +27,7 @@
let show = false;
let reloadOnWizardClose = false;
onMount(async () => {
request();
});
onMount(request);
const limit = 5;
@@ -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,11 +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;
$: console.log(currentInvoice);
$: isTrial = new Date($organization?.billingStartDate).getTime() - today.getTime() > 0;
</script>
{#if $organization}
@@ -53,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>
@@ -67,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">
@@ -130,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
@@ -27,6 +27,7 @@
label: 'United States'
}
];
onMount(async () => {
addresses = await sdk.forConsole.billing.listAddresses();
@@ -61,8 +62,8 @@
streetAddress,
city,
state,
postalCode,
addressLine2
postalCode ? postalCode : undefined,
addressLine2 ? postalCode : undefined
);
await sdk.forConsole.billing.setBillingAddress($organization.$id, address.$id);
@@ -6,7 +6,7 @@
import { organization } from '$lib/stores/organization';
import { Dependencies } from '$lib/constants';
import { initializeStripe, isStripeInitialized, submitStripeCard } from '$lib/stores/stripe';
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import type { PaymentList } from '$lib/sdk/billing';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
@@ -18,6 +18,10 @@
let selectedPaymentMethodId: string;
let name: string;
let error: string;
let element: HTMLDivElement;
let loader: HTMLDivElement;
let observer: MutationObserver;
onMount(async () => {
methods = await sdk.forConsole.billing.listPaymentMethods();
@@ -28,6 +32,27 @@
selectedPaymentMethodId = isBackup
? $organization.backupPaymentMethodId
: $organization.paymentMethodId;
observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length > 0) {
for (let node of Array.from(mutation.addedNodes)) {
if (
node instanceof Element &&
node.className.toLowerCase().includes('__privatestripeelement')
) {
loader.style.display = 'none';
}
}
}
}
}
});
});
onDestroy(() => {
observer.disconnect();
});
async function handleSubmit() {
@@ -88,6 +113,9 @@
$: if (show) {
isStripeInitialized.set(false);
if (element) {
observer.observe(element, { childList: true });
}
}
$: if (!show) {
@@ -154,9 +182,13 @@
bind:value={name}
required
autofocus={true} />
<div id="payment-element" data-private>
<!-- Stripe will create form elements here -->
<div class="aw-stripe-container" data-private>
<div class="loader-container" bind:this={loader}>
<div class="loader"></div>
</div>
<div id="payment-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
</div>
</FormList>
{/if}
@@ -174,3 +206,17 @@
</Button>
</svelte:fragment>
</FakeModal>
<style lang="scss">
.aw-stripe-container {
min-height: 295px;
position: relative;
.loader-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
}
}
</style>
@@ -20,7 +20,6 @@
couponData = response;
coupon = null;
} catch (error) {
console.log('test');
couponData.code = coupon;
couponData.status = 'error';
$wizard.interceptorNotificationEnabled = false;
@@ -25,7 +25,7 @@
$addCreditWizardStore.paymentMethodId = method.$id;
invalidate(Dependencies.PAYMENT_METHODS);
} catch (e) {
console.log(e.message);
throw new Error('Something went wrong. Please try again.');
}
}
@@ -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}
@@ -42,7 +42,7 @@
} catch (e) {
addNotification({
type: 'error',
message: e.mesage
message: e.message
});
trackError(e, Submit.ProjectCreate);
}
@@ -52,7 +52,7 @@
$createProject = {
id: null,
name: null,
region: 'eu-de'
region: 'fra'
};
});
@@ -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 {
@@ -22,13 +20,13 @@
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import Delete from '../deleteMember.svelte';
import { toLocaleDate } from '$lib/helpers/date';
export let data: PageData;
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(
@@ -68,6 +66,7 @@
<TableHeader>
<TableCellHead width={140}>Name</TableCellHead>
<TableCellHead width={120}>Email</TableCellHead>
<TableCellHead width={100}>Joined</TableCellHead>
<TableCellHead width={90} />
<TableCellHead width={30} />
</TableHeader>
@@ -86,6 +85,9 @@
</div>
</TableCell>
<TableCellText title="Email">{member.userEmail}</TableCellText>
<TableCellText title="Joined">
{member.joined ? toLocaleDate(member.joined) : ''}
</TableCellText>
<TableCell>
{#if member.invited && !member.joined}
<Button
@@ -94,7 +96,7 @@
on:click={() => resend(member)}>Resend</Button>
{/if}
</TableCell>
<TableCell>
<TableCell right>
<button
class="button is-only-icon is-text"
aria-label="Delete item"
@@ -119,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);
@@ -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.
@@ -41,12 +41,18 @@ export const load: PageLoad = async ({ params, parent }) => {
sdk.forConsole.billing.listUsage(params.organization, startDate, endDate)
]);
const projectNames = await sdk.forConsole.projects.list([
Query.equal(
'$id',
usage.projects.map((p) => p.projectId)
)
]);
const queries: string[] = [];
if (usage?.projects?.length > 0) {
queries.push(
Query.equal(
'$id',
usage.projects.map((p) => p.projectId)
)
);
}
const projectNames = await sdk.forConsole.projects.list(queries);
return {
organizationUsage: usage,
@@ -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';
@@ -40,13 +40,13 @@
}
function format(value: number): string {
const humanized = humanFileSize(value);
switch (metric) {
case 'executions':
case 'users':
return abbreviateNumber(value);
case 'storage':
case 'bandwidth':
const humanized = humanFileSize(value);
return humanized.value + humanized.unit;
}
}
@@ -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>
@@ -7,5 +7,5 @@ export const createProject = writable<{
}>({
id: null,
name: null,
region: 'eu-de'
region: 'fra'
});
@@ -43,6 +43,7 @@
import { organization } from '$lib/stores/organization';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { wizard } from '$lib/stores/wizard';
import { BillingPlan } from '$lib/constants';
const projectId = $page.params.project;
@@ -117,7 +118,7 @@
</p>
<svelte:fragment slot="aside">
{#if $organization.billingPlan === 'tier-0'}
{#if $organization.billingPlan === BillingPlan.STARTER}
<Alert
buttons={[
{
@@ -3,7 +3,7 @@
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableScroll,
TableBody,
TableCell,
TableCellHead,
@@ -36,7 +36,7 @@
{/if}
</ContainerHeader>
{#if data.sessions.total}
<Table>
<TableScroll>
<TableHeader>
<TableCellHead width={140}>Browser and device</TableCellHead>
<TableCellHead width={140}>Session</TableCellHead>
@@ -85,7 +85,7 @@
</TableRow>
{/each}
</TableBody>
</Table>
</TableScroll>
{:else}
<EmptySearch>
<div class="u-flex u-flex-vertical u-cross-center u-gap-24">
@@ -68,9 +68,15 @@
<CardGrid>
<Heading tag="h6" size="7">Permissions</Heading>
<p>
Assign read or write permissions at the <b>collection level</b> or
<b>document level</b>. If collection level permissions are assigned, permissions applied
to individual documents are ignored.
A user requires appropriate permissions at either the <b>collection level</b> or
<b>document level</b> to access a document. If no permissions are configured, no user
can access the document
<a
href="https://appwrite.io/docs/products/databases/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about database permissions</a
>.
</p>
<svelte:fragment slot="aside">
@@ -66,8 +66,6 @@
]);
$updateCommandGroupRanks({ functions: 1000 });
$: console.log($functionsList);
</script>
<Container>
@@ -40,9 +40,7 @@
import DeploymentCreatedBy from './deploymentCreatedBy.svelte';
import DeploymentDomains from './deploymentDomains.svelte';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { readOnly, tierToPlan } from '$lib/stores/billing';
import { hoursToDays } from '$lib/helpers/date';
import { organization } from '$lib/stores/organization';
import { readOnly } from '$lib/stores/billing';
export let data;
@@ -215,19 +213,7 @@
<TableCellHead width={80}>Size</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody service="logs" total={isCloud ? Infinity : 0}>
<svelte:fragment slot="limit" let:limit let:upgradeMethod>
<p class="text">
Logs are retained in rolling {hoursToDays(limit)} intervals with the
{tierToPlan($organization.billingPlan).name}
plan.
<button
class="link"
type="button"
on:click|preventDefault={upgradeMethod}>Upgrade</button> to increase
your log retention for a longer period.
</p>
</svelte:fragment>
<TableBody>
{#each $deploymentList.deployments as deployment, index (deployment.$id)}
{@const status = deployment.status}
<TableRow>
@@ -1,7 +1,7 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Alert, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
@@ -65,7 +65,7 @@
{logs} hour of logs
</li>
</ul>
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<p class="text">
<button class="link" type="button" on:click|preventDefault={upgradeMethod}
>Upgrade</button>
@@ -58,7 +58,7 @@
<Heading tag="h6" size="7" id="schedule">Schedule</Heading>
<p>
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
href="https://appwrite.io/docs/products/functions/execution"
href="https://appwrite.io/docs/products/functions/execution#schedule"
target="_blank"
rel="noopener noreferrer"
class="link">
@@ -8,7 +8,7 @@
<svelte:fragment slot="title">Schedule</svelte:fragment>
<svelte:fragment slot="subtitle">
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
href="https://appwrite.io/docs/products/functions/quick-start"
href="https://appwrite.io/docs/products/functions/execution#schedule"
target="_blank"
rel="noopener noreferrer"
class="link">More details on Cron syntax here</a
@@ -155,7 +155,7 @@
<svelte:fragment slot="list">
<DropListItem on:click={() => addPlatform(Platform.Web)}>
Web
</DropListItem>Ï
</DropListItem>
<DropListItem on:click={() => addPlatform(Platform.Flutter)}>
Flutter
</DropListItem>
@@ -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">
@@ -202,7 +202,8 @@
<svelte:fragment slot="actions">
<Button
submit
disabled={isButtonDisabled || $organization.billingPlan === 'tier-0'}>
disabled={isButtonDisabled ||
$organization.billingPlan === BillingPlan.STARTER}>
Update
</Button>
</svelte:fragment>
@@ -19,6 +19,7 @@
import { formatNum } from '$lib/helpers/string';
import { total } from '$lib/layout/usage.svelte';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { BillingPlan } from '$lib/constants.js';
export let data;
@@ -51,28 +52,28 @@
<div class="u-flex u-cross-center u-main-space-between">
<Heading tag="h2" size="5">Usage</Heading>
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
<span class="text">Upgrade</span>
</Button>
{/if}
</div>
<div class="u-flex u-main-space-between common-section u-cross-center">
{#if $organization.billingPlan === 'tier-2'}
{#if $organization.billingPlan === BillingPlan.SCALE}
<p class="text">
On the Scale plan, you'll be charged only for any usage that exceeds the thresholds
per resource listed below. <Button
on:click={() => ($showUsageRatesModal = true)}
link>Learn more about plan usage limits.</Button>
</p>
{:else if $organization.billingPlan === 'tier-1'}
{:else if $organization.billingPlan === BillingPlan.PRO}
<p class="text">
On the Pro plan, you'll be charged only for any usage that exceeds the thresholds
per resource listed below. <Button
on:click={() => ($showUsageRatesModal = true)}
link>Learn more about plan usage limits.</Button>
</p>
{:else if $organization.billingPlan === 'tier-0'}
{:else if $organization.billingPlan === BillingPlan.STARTER}
<p class="text">
If you exceed the limits of the {plan} plan, services for your projects may be disrupted.
<button
@@ -53,7 +53,7 @@
const projectId = $page.params.project;
const bucketId = $page.params.bucket;
const usedStorage = bytesToSize(data.organizationUsage.storageTotal, 'MB');
const usedStorage = bytesToSize(data.organizationUsage.storageTotal, 'GB');
const getPreview = (fileId: string) =>
sdk.forProject.storage.getFilePreview(bucketId, fileId, 32, 32).toString() + '&mode=admin';
@@ -1,5 +1,6 @@
<script lang="ts">
import { Alert, CustomId } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Pill } from '$lib/elements';
import { Button, FormList, InputFile } from '$lib/elements/forms';
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
@@ -28,12 +29,12 @@
The {plan.name} plan has a maximum upload file size limit of {Math.floor(
parseInt(size.value)
)}{size.unit}.
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
Upgrade to allow files of a larger size.
{/if}
</p>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === 'tier-0'}
{#if $organization?.billingPlan === BillingPlan.STARTER}
<div class="alert-buttons u-flex">
<Button text on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
Upgrade plan
@@ -1,6 +1,7 @@
<script lang="ts">
import { Submit } from '$lib/actions/analytics';
import { Alert, CardGrid, Heading } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { Button, Form, FormItem, InputNumber, InputSelect } from '$lib/elements/forms';
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
import { createByteUnitPair } from '$lib/helpers/unit';
@@ -35,19 +36,19 @@
<p class="text">Set the maximum file size allowed in the bucket.</p>
<svelte:fragment slot="aside">
{#if isCloud}
{@const size = humanFileSize(sizeToBytes(service, 'MB', 1024))}
{@const size = humanFileSize(sizeToBytes(service, 'GB', 1024))}
{@const plan = tierToPlan($organization?.billingPlan)}
<Alert type="info">
<p class="text">
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
@@ -3,7 +3,6 @@ import type { Models } from '@appwrite.io/console';
import type { BarSeriesOption } from 'echarts/charts';
import { derived, writable } from 'svelte/store';
// TODO: remove region type once the SDK is updated
export const project = derived(
page,
($page) => $page.data.project as Models.Project & { region?: string }
@@ -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>
+3
View File
@@ -1,4 +1,5 @@
import { page } from '$app/stores';
import type { HeaderAlert } from '$lib/stores/headerAlert';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
@@ -9,3 +10,5 @@ export const consoleVariables = derived(
);
export const showPostReleaseModal = writable(false);
export const activeHeaderAlert = writable<HeaderAlert>(null);
@@ -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;
@@ -19,10 +20,9 @@
const card = await sdk.forConsole.billing.getPaymentMethod(
$createOrganization.paymentMethodId
);
console.log(card);
return card;
} catch (error) {
console.log(error);
throw new Error(error.message);
}
}
@@ -30,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>
@@ -54,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>
@@ -76,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>
@@ -92,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>
@@ -80,7 +81,7 @@
value="tier-0"
disabled={!!anyOrgFree}
tooltipText="You are limited to 1 Free organization per account."
showTooltip={!!anyOrgFree}>
tooltipShow={!!anyOrgFree}>
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"

Some files were not shown because too many files have changed in this diff Show More