mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-pwnd
This commit is contained in:
@@ -3,8 +3,14 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
|
||||
env:
|
||||
VITE_APPWRITE_ENDPOINT: http://appwrite.test/v1
|
||||
|
||||
Generated
+7647
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16">
|
||||
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<details class="collapsible-wrapper" {open}>
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class="collapsible-button"
|
||||
class="collapsible-button u-position-relative"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click
|
||||
role="button"
|
||||
|
||||
@@ -67,11 +67,11 @@
|
||||
on:click={handleBLur}
|
||||
bind:this={backdrop}>
|
||||
<div
|
||||
class="modal payment-modal"
|
||||
class="modal"
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class:is-separate-header={headerDivider}>
|
||||
<Form isModal {onSubmit} class="payment-form">
|
||||
<Form isModal {onSubmit}>
|
||||
<header class="modal-header">
|
||||
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
|
||||
<div class="u-flex u-cross-center u-gap-16">
|
||||
@@ -151,14 +151,4 @@
|
||||
background-color: hsl(240 5% 8% / 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-modal {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.payment-form {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
<div>
|
||||
<form on:submit|preventDefault={addFilter}>
|
||||
<div class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<InputSelect
|
||||
id="column"
|
||||
options={$columns.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'}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
import { isSupportOnline, showSupportModal } from '../../routes/console/wizard/support/store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: isPaid = $organization?.billingPlan === 'tier-1' || $organization?.billingPlan === 'tier-2';
|
||||
$: isPaid =
|
||||
$organization?.billingPlan === BillingPlan.PRO ||
|
||||
$organization?.billingPlan === BillingPlan.SCALE;
|
||||
</script>
|
||||
|
||||
{#if isCloud}
|
||||
@@ -39,7 +42,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $organization?.billingPlan === 'tier-0'}
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
<Button fullWidth href="https://appwrite.io/pricing" external>
|
||||
<span class="text">Get Premium support</span>
|
||||
</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum Dependencies {
|
||||
ADDRESS = 'dependency:address',
|
||||
PAYMENT_METHODS = 'dependency:paymentMethods',
|
||||
ORGANIZATION = 'dependency:organization',
|
||||
MEMBERS = 'dependency:members',
|
||||
PROJECT = 'dependency:project',
|
||||
PROJECT_VARIABLES = 'dependency:project_variables',
|
||||
PROJECT_INSTALLATIONS = 'dependency:project_installations',
|
||||
@@ -616,3 +617,9 @@ export const limitRates = {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export enum BillingPlan {
|
||||
STARTER = 'tier-0',
|
||||
PRO = 'tier-1',
|
||||
SCALE = 'tier-2'
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
let classes: string = undefined;
|
||||
export { classes as class };
|
||||
export let actions: MultiActionArray = [];
|
||||
export let submissionLoader = false;
|
||||
|
||||
const isSubmitting = hasContext('form')
|
||||
? getContext<FormContext>('form').isSubmitting
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export let showLabel = true;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
export let value: string;
|
||||
export let required = false;
|
||||
export let nullable = false;
|
||||
export let disabled = false;
|
||||
@@ -35,6 +35,11 @@
|
||||
error = element.validationMessage;
|
||||
}
|
||||
|
||||
function handleInput(event: Event) {
|
||||
const { value: currentValue } = event.currentTarget as HTMLInputElement;
|
||||
value = currentValue || null;
|
||||
}
|
||||
|
||||
let prevValue = '';
|
||||
function handleNullChange(e: CustomEvent<boolean>) {
|
||||
const isNull = e.detail;
|
||||
@@ -64,12 +69,13 @@
|
||||
{disabled}
|
||||
{readonly}
|
||||
{required}
|
||||
{value}
|
||||
step=".001"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="datetime-local"
|
||||
class="input-text"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:input={handleInput}
|
||||
on:invalid={handleInvalid}
|
||||
style:--amount-of-buttons={isNullable ? 2.75 : 1}
|
||||
style:--button-size={isNullable ? '2rem' : '1rem'} />
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let icon = 'info';
|
||||
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`;
|
||||
const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._\-]*$`;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
import { tierToPlan, type PlanServices } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
export let logs: Models.LogList;
|
||||
export let offset = 0;
|
||||
@@ -50,8 +52,10 @@
|
||||
Logs are retained in rolling {hoursToDays(limit)} intervals with the
|
||||
{tierToPlan($organization.billingPlan).name}
|
||||
plan.
|
||||
<button class="link" type="button" on:click|preventDefault={upgradeMethod}
|
||||
>Upgrade</button> to increase your log retention for a longer period.
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
<Button link on:click={upgradeMethod}>Upgrade</Button> to increase your log
|
||||
retention for a longer period.
|
||||
{/if}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
{#each logs.logs as log}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
export let title: string;
|
||||
export let tooltipContent =
|
||||
$organization.billingPlan === 'tier-0'
|
||||
$organization.billingPlan === BillingPlan.STARTER
|
||||
? `Upgrade to add more ${title.toLocaleLowerCase()}`
|
||||
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
|
||||
tierToPlan($organization.billingPlan).name
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import { Pill } from '$lib/elements';
|
||||
import { showExcess } from '$routes/console/organization-[organization]/store';
|
||||
import { readOnly } from '$lib/stores/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
let showDropdown = false;
|
||||
let showSupport = false;
|
||||
@@ -117,10 +118,9 @@
|
||||
|
||||
<div class="main-header-end">
|
||||
<nav class="u-flex is-only-desktop u-cross-center">
|
||||
{#if isCloud && $organization?.billingPlan === 'tier-0' && !$page.url.pathname.startsWith('/console/account')}
|
||||
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
|
||||
<Button
|
||||
disabled={$organization?.markedForDeletion}
|
||||
secondary
|
||||
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
|
||||
Upgrade
|
||||
</Button>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
import { getServiceLimit, tierToPlan } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
let selectedRequest = 'parameters';
|
||||
let selectedResponse = 'logs';
|
||||
@@ -310,13 +312,14 @@
|
||||
<Alert>
|
||||
Logs are retained in rolling {hoursToDays(limit)} intervals
|
||||
with the {tier} plan.
|
||||
<button
|
||||
class="link"
|
||||
type="button"
|
||||
on:click|preventDefault={() =>
|
||||
wizard.start(ChangeOrganizationTierCloud)}
|
||||
>Upgrade</button> to increase your log retention for
|
||||
a longer period.
|
||||
{#if $organization.billingPlan === BillingPlan.STARTER}
|
||||
<Button
|
||||
link
|
||||
on:click={() =>
|
||||
wizard.start(ChangeOrganizationTierCloud)}
|
||||
>Upgrade</Button> to increase your log retention
|
||||
for a longer period.
|
||||
{/if}
|
||||
</Alert>
|
||||
{/if}
|
||||
<Code withCopy noMargin code={execution.logs} language="sh" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
import { slide } from '$lib/helpers/transition';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
} else {
|
||||
$wizard.step--;
|
||||
}
|
||||
wizard.setInterceptor(null);
|
||||
trackEvent('wizard_back');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,17 @@
|
||||
|
||||
function handleSubmit() {
|
||||
dispatch('exit');
|
||||
show = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Exit Process" bind:show onSubmit={handleSubmit} icon="exclamation" state="warning">
|
||||
<Modal
|
||||
title="Exit Process"
|
||||
bind:show
|
||||
onSubmit={handleSubmit}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p>
|
||||
Are you sure you want to exit from <slot />? All data will be deleted. This action is
|
||||
irreversible.
|
||||
|
||||
+19
-13
@@ -185,15 +185,15 @@ export type AggregationList = {
|
||||
};
|
||||
|
||||
export type AllowedRegions =
|
||||
| 'eu-de'
|
||||
| 'us-nyc'
|
||||
| 'us-sfo'
|
||||
| 'ap-in'
|
||||
| 'eu-gb'
|
||||
| 'eu-nl'
|
||||
| 'ap-sg'
|
||||
| 'ap-ca'
|
||||
| 'ap-au'
|
||||
| 'fra'
|
||||
| 'nyc'
|
||||
| 'sfo'
|
||||
| 'blr'
|
||||
| 'lon'
|
||||
| 'ams'
|
||||
| 'sgp'
|
||||
| 'tor'
|
||||
| 'syd'
|
||||
| 'default'; //TODO: remove after migration
|
||||
|
||||
export type Region = {
|
||||
@@ -265,6 +265,8 @@ export type PlansInfo = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type PlansMap = Map<Tier, Plan>;
|
||||
|
||||
export class Billing {
|
||||
client: Client;
|
||||
|
||||
@@ -276,14 +278,16 @@ export class Billing {
|
||||
organizationId: string,
|
||||
name: string,
|
||||
billingPlan: string,
|
||||
paymentMethodId: string
|
||||
paymentMethodId: string,
|
||||
billingAddressId: string
|
||||
): Promise<Organization> {
|
||||
const path = `/organizations`;
|
||||
const params = {
|
||||
organizationId,
|
||||
name,
|
||||
billingPlan,
|
||||
paymentMethodId
|
||||
paymentMethodId,
|
||||
billingAddressId
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
@@ -331,13 +335,15 @@ export class Billing {
|
||||
async updatePlan(
|
||||
organizationId: string,
|
||||
billingPlan: string,
|
||||
paymentMethodId: string
|
||||
paymentMethodId: string,
|
||||
billingAddressId: string
|
||||
): Promise<Organization> {
|
||||
const path = `/organizations/${organizationId}/plan`;
|
||||
const params = {
|
||||
organizationId,
|
||||
billingPlan,
|
||||
paymentMethodId
|
||||
paymentMethodId,
|
||||
billingAddressId
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
|
||||
+29
-44
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import type { Tier } from '$lib/stores/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { LayoutLoad } from './$types';
|
||||
@@ -18,9 +20,13 @@ export const load: LayoutLoad = async ({ fetch, depends }) => {
|
||||
|
||||
const [data, variables] = await Promise.all([versionPromise, variablesPromise]);
|
||||
|
||||
let plansInfo = null;
|
||||
let plansInfo = new Map<Tier, Plan>();
|
||||
if (isCloud) {
|
||||
plansInfo = await sdk.forConsole.billing.getPlansInfo();
|
||||
const plansArray = await sdk.forConsole.billing.getPlansInfo();
|
||||
plansInfo = plansArray.plans.reduce((map, plan) => {
|
||||
map.set(plan.$id as Tier, plan);
|
||||
return map;
|
||||
}, new Map<Tier, Plan>());
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,9 @@ export const load: PageLoad = async ({ parent, url }) => {
|
||||
|
||||
if (organizations.total) {
|
||||
const teamId = account.prefs.organization ?? organizations.teams[0].$id;
|
||||
throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
|
||||
if (!teamId) {
|
||||
throw redirect(303, `${base}/console/account/organizations${url.search ?? ''}`);
|
||||
} else throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
|
||||
} else {
|
||||
throw redirect(303, `${base}/console/onboarding${url.search ?? ''}`);
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Card } from '$lib/components';
|
||||
import { Card, Heading } from '$lib/components';
|
||||
import CustomId from '$lib/components/customId.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { Button, Form, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import FormList from '$lib/elements/forms/formList.svelte';
|
||||
import { Container } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -16,10 +16,18 @@
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
|
||||
import { tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { createOrganization } from '../wizard/cloudOrganization/store';
|
||||
|
||||
let name: string;
|
||||
let id: string;
|
||||
let showCustomId = false;
|
||||
let plan: Tier;
|
||||
|
||||
const options = [
|
||||
{ value: BillingPlan.STARTER, label: 'Starter - $0/month' },
|
||||
{ value: BillingPlan.PRO, label: 'Pro - $15/month + add-ons' }
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
if (isCloud) {
|
||||
@@ -32,65 +40,120 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function createProject() {
|
||||
try {
|
||||
const org = await createOrganization();
|
||||
const project = await sdk.forConsole.projects.create(
|
||||
id ?? ID.unique(),
|
||||
name,
|
||||
org.$id,
|
||||
isCloud ? 'eu-de' : 'default'
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
goto(`/console/project-${project.$id}`);
|
||||
trackEvent(Submit.ProjectCreate, {
|
||||
customId: !!id,
|
||||
teamId: org.$id
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.ProjectCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrganization() {
|
||||
async function handleSubmit() {
|
||||
const orgName = name?.length ? name : 'Personal Projects';
|
||||
if (isCloud) {
|
||||
return await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
'tier-0',
|
||||
null
|
||||
);
|
||||
} else return await sdk.forConsole.teams.create(ID.unique(), 'Personal Projects');
|
||||
if (plan === BillingPlan.STARTER) {
|
||||
try {
|
||||
const org = await sdk.forConsole.billing.createOrganization(
|
||||
id ?? ID.unique(),
|
||||
orgName,
|
||||
plan,
|
||||
null,
|
||||
null
|
||||
);
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
customId: !!id,
|
||||
plan: tierToPlan(plan)?.name
|
||||
});
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
addNotification({
|
||||
message: `${orgName} organization successfully created`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.OrganizationCreate);
|
||||
}
|
||||
} else {
|
||||
wizard.start(CreateOrganizationCloud, null, 2);
|
||||
$createOrganization.name = orgName;
|
||||
$createOrganization.billingPlan = plan;
|
||||
$createOrganization.id = id;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const org = await sdk.forConsole.teams.create(id ?? ID.unique(), orgName);
|
||||
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
|
||||
addNotification({
|
||||
message: `${orgName} organization successfully created`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.OrganizationCreate);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container overlapCover size="large">
|
||||
<Card>
|
||||
<Form onSubmit={createProject}>
|
||||
<FormList>
|
||||
<Heading size="4" tag="h2">Create a new organization</Heading>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormList gap={16}>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Project name"
|
||||
placeholder="First Appwrite Project"
|
||||
required
|
||||
label="Name"
|
||||
placeholder="Organization name"
|
||||
hideRequired
|
||||
bind:value={name} />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}>
|
||||
<span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Project ID
|
||||
Organization ID
|
||||
</span>
|
||||
</Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
|
||||
<CustomId bind:show={showCustomId} name="Organization" isProject bind:id />
|
||||
{/if}
|
||||
<Button fullWidth submit disabled={name === ''} event="create_project">
|
||||
Create project
|
||||
{#if isCloud}
|
||||
<div class="u-margin-block-start-8">
|
||||
<h3><b>Plan</b></h3>
|
||||
<p>
|
||||
For more details on our plans, visit our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/pricing">pricing page</Button
|
||||
>.
|
||||
</p>
|
||||
<FormList class="u-margin-block-start-8">
|
||||
<InputSelect
|
||||
placeholder="Select a plan"
|
||||
label="Select plan"
|
||||
showLabel={false}
|
||||
id="plan"
|
||||
required
|
||||
hideRequired
|
||||
{options}
|
||||
bind:value={plan} />
|
||||
</FormList>
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
fullWidth
|
||||
submit
|
||||
disabled={isCloud && !plan}
|
||||
event="create_organization"
|
||||
submissionLoader
|
||||
let:isSubmitting>
|
||||
{#if isSubmitting}
|
||||
Creating your first organization
|
||||
{:else}
|
||||
Get started
|
||||
{/if}
|
||||
</Button>
|
||||
</FormList>
|
||||
</Form>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { Heading } from '$lib/components';
|
||||
import { Cover } from '$lib/layout';
|
||||
</script>
|
||||
|
||||
<Cover size="large">
|
||||
<CoverTitle>Let's create your first project</CoverTitle>
|
||||
<div class="u-flex u-cross-center u-main-center u-width-full-line">
|
||||
<Heading size="1" tag="h1" trimmed={false}>Welcome to Appwrite</Heading>
|
||||
</div>
|
||||
</Cover>
|
||||
|
||||
@@ -9,9 +9,11 @@ import Header from './header.svelte';
|
||||
import { headerAlert } from '$lib/stores/headerAlert';
|
||||
import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
depends(Dependencies.MEMBERS);
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
|
||||
if (isCloud) {
|
||||
@@ -28,6 +30,10 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
const newPrefs = { ...prefs, organization: params.organization };
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
preferences.loadTeamPrefs(params.organization);
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
@@ -35,6 +41,9 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
members: await sdk.forConsole.teams.listMemberships(params.organization)
|
||||
};
|
||||
} catch (e) {
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
const newPrefs = { ...prefs, organization: null };
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
localStorage.removeItem('organization');
|
||||
throw error(e.code, e.message);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
import type { RegionList } from '$lib/sdk/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -136,93 +137,96 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<Heading tag="h2" size="5">Projects</Heading>
|
||||
{#if $organization?.$id}
|
||||
<Container>
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<Heading tag="h2" size="5">Projects</Heading>
|
||||
|
||||
<DropList bind:show={showDropdown} placement="bottom-end">
|
||||
<Button
|
||||
on:click={handleCreateProject}
|
||||
event="create_project"
|
||||
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create project</span>
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
|
||||
<DropListItem on:click={importProject}>
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
Import project <span class="tag eyebrow-heading-3">Experimental</span>
|
||||
</div>
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
<DropList bind:show={showDropdown} placement="bottom-end">
|
||||
<Button
|
||||
on:click={handleCreateProject}
|
||||
event="create_project"
|
||||
disabled={$readOnly && !GRACE_PERIOD_OVERRIDE}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create project</span>
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => (showCreate = true)}>Empty project</DropListItem>
|
||||
<DropListItem on:click={importProject}>
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
Import project <span class="tag eyebrow-heading-3">Experimental</span>
|
||||
</div>
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
|
||||
{#if data.projects.total}
|
||||
<CardContainer
|
||||
total={data.projects.total}
|
||||
offset={data.offset}
|
||||
on:click={handleCreateProject}>
|
||||
{#each data.projects.projects as project}
|
||||
<li>
|
||||
<GridItem1 href={`${base}/console/project-${project.$id}`}>
|
||||
<svelte:fragment slot="eyebrow">
|
||||
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
{project.name}
|
||||
</svelte:fragment>
|
||||
{#if allServiceDisabled(project)}
|
||||
<p>
|
||||
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
|
||||
</p>
|
||||
{/if}
|
||||
{@const platforms = filterPlatforms(
|
||||
project.platforms.map((platform) => getPlatformInfo(platform.type))
|
||||
)}
|
||||
{#each platforms as platform, i}
|
||||
{#if i < 3}
|
||||
{#if data.projects.total}
|
||||
<CardContainer
|
||||
total={data.projects.total}
|
||||
offset={data.offset}
|
||||
on:click={handleCreateProject}>
|
||||
{#each data.projects.projects as project}
|
||||
{@const platforms = filterPlatforms(
|
||||
project.platforms.map((platform) => getPlatformInfo(platform.type))
|
||||
)}
|
||||
<li>
|
||||
<GridItem1 href={`${base}/console/project-${project.$id}`}>
|
||||
<svelte:fragment slot="eyebrow">
|
||||
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
{project.name}
|
||||
</svelte:fragment>
|
||||
{#if allServiceDisabled(project)}
|
||||
<p>
|
||||
<span class="icon-pause" aria-hidden="true" /> All services are disabled.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#each platforms as platform, i}
|
||||
{#if i < 3}
|
||||
<Pill>
|
||||
<span class={`icon-${platform.icon}`} aria-hidden="true" />
|
||||
{platform.name}
|
||||
</Pill>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if platforms?.length > 3}
|
||||
<Pill>
|
||||
<span class={`icon-${platform.icon}`} aria-hidden="true" />
|
||||
{platform.name}
|
||||
+{project.platforms.length - 3}
|
||||
</Pill>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if platforms?.length > 3}
|
||||
<Pill>
|
||||
+{project.platforms.length - 3}
|
||||
</Pill>
|
||||
{/if}
|
||||
<svelte:fragment slot="icons">
|
||||
{#if isCloud && regions}
|
||||
{@const region = findRegion(project)}
|
||||
<span class="u-color-text-gray">
|
||||
{region.name}
|
||||
</span>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</GridItem1>
|
||||
</li>
|
||||
{/each}
|
||||
<svelte:fragment slot="empty">
|
||||
<p>Create a new project</p>
|
||||
</svelte:fragment>
|
||||
</CardContainer>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
on:click={handleCreateProject}
|
||||
target="project"
|
||||
href="https://appwrite.io/docs/quick-starts"></Empty>
|
||||
{/if}
|
||||
<svelte:fragment slot="icons">
|
||||
{#if isCloud && regions}
|
||||
{@const region = findRegion(project)}
|
||||
<span class="u-color-text-gray u-medium u-line-height-2">
|
||||
{region?.name}
|
||||
</span>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</GridItem1>
|
||||
</li>
|
||||
{/each}
|
||||
<svelte:fragment slot="empty">
|
||||
<p>Create a new project</p>
|
||||
</svelte:fragment>
|
||||
</CardContainer>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
on:click={handleCreateProject}
|
||||
target="project"
|
||||
href="https://appwrite.io/docs/quick-starts"></Empty>
|
||||
{/if}
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Projects"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.projects.total} />
|
||||
</Container>
|
||||
<PaginationWithLimit
|
||||
name="Projects"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.projects.total} />
|
||||
</Container>
|
||||
|
||||
<CreateOrganization bind:show={addOrganization} />
|
||||
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
|
||||
<CreateOrganization bind:show={addOrganization} />
|
||||
<CreateProject bind:show={showCreate} teamId={$page.params.organization} />
|
||||
{/if}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
|
||||
projects: await sdk.forConsole.projects.list([
|
||||
Query.offset(offset),
|
||||
Query.equal('teamId', params.organization),
|
||||
Query.limit(CARD_LIMIT),
|
||||
Query.limit(limit),
|
||||
Query.orderDesc('')
|
||||
])
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
+25
-27
@@ -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">
|
||||
|
||||
+9
-3
@@ -68,9 +68,15 @@
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Permissions</Heading>
|
||||
<p>
|
||||
Assign read or write permissions at the <b>collection level</b> or
|
||||
<b>document level</b>. If collection level permissions are assigned, permissions applied
|
||||
to individual documents are ignored.
|
||||
A user requires appropriate permissions at either the <b>collection level</b> or
|
||||
<b>document level</b> to access a document. If no permissions are configured, no user
|
||||
can access the document
|
||||
<a
|
||||
href="https://appwrite.io/docs/products/databases/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more about database permissions</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Alert, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
@@ -65,7 +65,7 @@
|
||||
{logs} hour of logs
|
||||
</li>
|
||||
</ul>
|
||||
{#if $organization?.billingPlan === 'tier-0'}
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
<p class="text">
|
||||
<button class="link" type="button" on:click|preventDefault={upgradeMethod}
|
||||
>Upgrade</button>
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@
|
||||
<Heading tag="h6" size="7" id="schedule">Schedule</Heading>
|
||||
<p>
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://appwrite.io/docs/products/functions/execution"
|
||||
href="https://appwrite.io/docs/products/functions/execution#schedule"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<svelte:fragment slot="title">Schedule</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Set a Cron schedule to trigger your function. Leave blank for no schedule. <a
|
||||
href="https://appwrite.io/docs/products/functions/quick-start"
|
||||
href="https://appwrite.io/docs/products/functions/execution#schedule"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">More details on Cron syntax here</a
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Alert, CustomId } from '$lib/components';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button, FormList, InputFile } from '$lib/elements/forms';
|
||||
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
|
||||
@@ -28,12 +29,12 @@
|
||||
The {plan.name} plan has a maximum upload file size limit of {Math.floor(
|
||||
parseInt(size.value)
|
||||
)}{size.unit}.
|
||||
{#if $organization?.billingPlan === 'tier-0'}
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
Upgrade to allow files of a larger size.
|
||||
{/if}
|
||||
</p>
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === 'tier-0'}
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
<div class="alert-buttons u-flex">
|
||||
<Button text on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
|
||||
Upgrade plan
|
||||
|
||||
+4
-3
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user