Merge branch 'main' of https://github.com/appwrite/console into 1.6.x
@@ -1,22 +1,17 @@
|
||||
name: E2E
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
branches: ['**']
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
|
||||
env:
|
||||
VITE_STRIPE_PUBLIC_KEY: ${{ vars.VITE_STRIPE_PUBLIC_KEY }}
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "npm:matej-appwrite-console-16x@0.6.7",
|
||||
"@appwrite.io/pink": "0.23.0",
|
||||
"@appwrite.io/pink-icons": "0.23.0",
|
||||
"@appwrite.io/pink": "0.25.0",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
|
||||
@@ -8,7 +8,9 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 120000,
|
||||
env: {
|
||||
VITE_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
|
||||
VITE_CONSOLE_MODE: 'cloud'
|
||||
VITE_CONSOLE_MODE: 'cloud',
|
||||
VITE_STRIPE_PUBLIC_KEY:
|
||||
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
|
||||
},
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
|
||||
@@ -135,6 +135,25 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<script>
|
||||
let themeInUse = 'auto';
|
||||
const appwrite = localStorage.getItem('appwrite');
|
||||
|
||||
if (appwrite) {
|
||||
try {
|
||||
const localPrefs = JSON.parse(appwrite);
|
||||
if (localPrefs) themeInUse = localPrefs.theme;
|
||||
} catch (ignore) {
|
||||
// Ignore any errors during JSON parsing
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
themeInUse =
|
||||
themeInUse === 'auto' ? (systemPrefersDark ? 'dark' : 'light') : themeInUse;
|
||||
|
||||
document.body.setAttribute('class', `theme-${themeInUse}`);
|
||||
</script>
|
||||
<div id="svelte">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { activeHeaderAlert } from '$routes/console/store';
|
||||
import GradientBanner from '../gradientBanner.svelte';
|
||||
|
||||
let show = true;
|
||||
|
||||
function handleClose() {
|
||||
show = false;
|
||||
const now = new Date().getTime();
|
||||
localStorage.setItem($activeHeaderAlert.id, now.toString());
|
||||
trackEvent('close_upgrade_banner', {
|
||||
source: 'cloud_credits_banner',
|
||||
campaign: 'WelcomeManual'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !$page.url.pathname.includes('/console/account')}
|
||||
<GradientBanner on:close={handleClose}>
|
||||
<div class="u-flex u-gap-24 u-main-center u-cross-center u-flex-vertical-mobile">
|
||||
<span class="body-text-1">Get $50 Cloud credits for Appwrite Pro.</span>
|
||||
<Button
|
||||
secondary
|
||||
fullWidthMobile
|
||||
class="u-line-height-1"
|
||||
href={`${base}/console/apply-credit?code=appw50&org=${$organization.$id}`}
|
||||
on:click={() => {
|
||||
trackEvent('click_credits_redeem', {
|
||||
from: 'button',
|
||||
source: 'cloud_credits_banner',
|
||||
campaign: 'WelcomeManual'
|
||||
});
|
||||
}}>
|
||||
Claim credits
|
||||
</Button>
|
||||
</div>
|
||||
</GradientBanner>
|
||||
{/if}
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<div class="top-banner">
|
||||
<div class="top-banner-bg">
|
||||
<div class="top-banner-bg-1">
|
||||
<img src="/images/top-banner/bg-pink-desktop.svg" width="1283" height="1278" alt="" />
|
||||
</div>
|
||||
<div class="top-banner-bg-2">
|
||||
<img src="/images/top-banner/bg-mint-desktop.svg" width="1051" height="1271" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-banner-content u-color-text-primary">
|
||||
<slot />
|
||||
</div>
|
||||
<button
|
||||
on:click|preventDefault={() => dispatch('close')}
|
||||
type="button"
|
||||
class="top-banner-button"
|
||||
aria-label="close upgrade message">
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,14 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { Box, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
|
||||
let selectedTab: 'tier-0' | 'tier-1' = 'tier-0';
|
||||
export let downgrade = false;
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
</script>
|
||||
|
||||
<Box>
|
||||
<Card>
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === 'tier-0'}
|
||||
@@ -25,16 +26,66 @@
|
||||
<div class="u-margin-block-start-24">
|
||||
{#if selectedTab === 'tier-0'}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
<li>
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions} Functions
|
||||
per project
|
||||
</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
</ul>
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text"> Limited to 1 organization member </span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{plan.bandwidth}GB bandwidth
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{plan.storage}GB storage
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{formatNum(plan.executions)} executions
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
{:else}
|
||||
<ul class="u-margin-block-start-8 un-order-list">
|
||||
<li>
|
||||
<span class="text">
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text"> Limited to 1 organization member </span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text">
|
||||
{plan.bandwidth}GB bandwidth
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text">
|
||||
{plan.storage}GB storage
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text">
|
||||
{formatNum(plan.executions)} executions
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if selectedTab === 'tier-1'}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
@@ -47,4 +98,4 @@
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import type { OrganizationUsage } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
export let tier: Tier;
|
||||
|
||||
@@ -67,11 +68,15 @@
|
||||
</svelte:fragment>
|
||||
</Alert>
|
||||
|
||||
<TableScroll dense noMargin class="u-margin-block-start-16">
|
||||
<TableScroll noMargin dense class="u-margin-block-start-16">
|
||||
<TableHeader>
|
||||
<TableCellHead>Resource</TableCellHead>
|
||||
<TableCellHead>Free limit</TableCellHead>
|
||||
<TableCellHead>Excess usage</TableCellHead>
|
||||
<TableCellHead>
|
||||
Excess usage <span
|
||||
use:tooltip={{ content: 'Metrics are estimates updated every 24 hours' }}
|
||||
class="icon-info"></span>
|
||||
</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#if excess?.members}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button, InputChoice, InputSelectSearch, InputText } from '$lib/elements/forms';
|
||||
import { Button, Helper, InputChoice, InputSelectSearch, InputText } from '$lib/elements/forms';
|
||||
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
@@ -14,6 +14,18 @@
|
||||
|
||||
let showTaxId = false;
|
||||
let showPaymentModal = false;
|
||||
let input: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
function handleInvalid(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (input.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
error = input.validationMessage;
|
||||
}
|
||||
|
||||
async function cardSaved(event: CustomEvent<PaymentMethodData>) {
|
||||
value = event.detail.$id;
|
||||
@@ -85,22 +97,33 @@
|
||||
</svelte:fragment>
|
||||
</InputSelectSearch>
|
||||
{:else}
|
||||
<Card
|
||||
isDashed
|
||||
style="--p-card-padding:0.75rem; --p-card-bg-color: transparent; --p-card-border-radius: 0.5rem"
|
||||
isTile>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p class="u-flex u-gap-8 u-cross-center">
|
||||
<span
|
||||
class="icon-exclamation u-color-text-warning"
|
||||
style:font-size="var(--icon-size-medium)"></span>
|
||||
<span class="text">A payment method is required</span>
|
||||
</p>
|
||||
<Button secondary on:click={() => (showPaymentModal = true)}>
|
||||
<span class="icon-plus"></span> <span class="text">Add</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
<input
|
||||
bind:this={input}
|
||||
on:invalid={handleInvalid}
|
||||
required
|
||||
class="u-hide"
|
||||
type="text"
|
||||
name="method"
|
||||
id="method" />
|
||||
<Card
|
||||
isDashed
|
||||
style="--p-card-padding:0.75rem; --p-card-bg-color: transparent; --p-card-border-radius: 0.5rem"
|
||||
isTile>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p>
|
||||
<span class="icon-exclamation-circle"></span>
|
||||
<span class="text">No saved payment methods</span>
|
||||
</p>
|
||||
<Button secondary on:click={() => (showPaymentModal = true)}>
|
||||
<span class="icon-plus"></span> <span class="text">Add</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
{#if error}
|
||||
<Helper class="u-position-relative" type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showPaymentModal && isCloud && hasStripePublicKey}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
export let href: string = null;
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
export let style: string = '';
|
||||
export let style = '';
|
||||
|
||||
function getElement() {
|
||||
switch (true) {
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
// TODO: remove once pink is updated
|
||||
.collapsible-item {
|
||||
.collapsible-wrapper.is-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
&[open] .icon-cheveron-down {
|
||||
rotate: unset;
|
||||
}
|
||||
|
||||
cursor: not-allowed;
|
||||
* {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { getNextTier, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import Card from './card.svelte';
|
||||
|
||||
export let source = 'empty_state_card';
|
||||
export let noAspectRatio = false;
|
||||
</script>
|
||||
|
||||
<Card style="--card-padding:1.5rem; --card-padding-mobile: 2rem">
|
||||
<div class="u-flex u-gap-24 u-flex-vertical-mobile">
|
||||
{#if $$slots?.image}
|
||||
<div class="u-stretch">
|
||||
<div
|
||||
style:--p-file-preview-border-color="transparent"
|
||||
class="file-preview is-full-cover-image u-width-full-line u-height-100-percent"
|
||||
style={noAspectRatio ? 'aspect-ratio: auto' : ''}>
|
||||
<slot name="image" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="u-stretch u-flex-vertical">
|
||||
<h3 class="body-text-2 u-bold"><slot name="title" /></h3>
|
||||
<p class="u-margin-block-start-8">
|
||||
<slot nextTier={tierToPlan(getNextTier($organization.billingPlan)).name} />
|
||||
</p>
|
||||
<Button
|
||||
class="u-margin-block-start-32"
|
||||
secondary
|
||||
fullWidth
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source
|
||||
});
|
||||
}}>Upgrade plan</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -2,11 +2,13 @@
|
||||
export let value: number = null;
|
||||
</script>
|
||||
|
||||
<fieldset class="u-margin-block-start-8">
|
||||
<fieldset class="u-margin-block-start-8 u-min-width-0">
|
||||
<legend class="label is-required u-margin-0 u-line-height-1-5 u-bold">
|
||||
<slot />
|
||||
</legend>
|
||||
<ul class="u-flex u-main-space-between u-margin-block-start-16">
|
||||
<ol
|
||||
class="u-flex u-main-space-between u-margin-block-start-16 u-overflow-x-auto"
|
||||
style="padding-block: 0.13rem">
|
||||
{#each Array(11) as _, i}
|
||||
<li>
|
||||
<input
|
||||
@@ -18,9 +20,9 @@
|
||||
checked={value === i} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</ol>
|
||||
<div class="u-flex u-main-space-between u-margin-block-start-8">
|
||||
<span>Not at all likely</span>
|
||||
<span>Extremely likely</span>
|
||||
<span class:u-color-text-gray={value === null}>Not at all likely</span>
|
||||
<span class:u-color-text-gray={value === null}>Extremely likely</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Feedback submitted successfully'
|
||||
message: 'Thank you for your feedback'
|
||||
});
|
||||
feedback.toggleFeedback();
|
||||
feedbackData.reset();
|
||||
@@ -33,15 +33,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="drop-section">
|
||||
<section class="drop-section u-padding-24">
|
||||
<header class="u-flex u-main-space-between u-gap-16">
|
||||
<h4 class="body-text-1 u-bold">{$selectedFeedback.title}</h4>
|
||||
<h4 class="label u-bold" style:font-size="var(--font-size-1)">{$selectedFeedback.title}</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
aria-label="Close feedback"
|
||||
title="Close feedback"
|
||||
on:click={() => feedback.toggleFeedback()}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
import Evaluation from './evaluation.svelte';
|
||||
</script>
|
||||
|
||||
<Evaluation bind:value={$feedbackData.value}>
|
||||
How likely are you to recommend Appwrite to a friend or colleague?
|
||||
</Evaluation>
|
||||
{#if $feedbackData.value}
|
||||
<FormList>
|
||||
<FormList>
|
||||
<Evaluation bind:value={$feedbackData.value}>
|
||||
How likely are you to recommend Appwrite to a friend or colleague?
|
||||
</Evaluation>
|
||||
{#if $feedbackData.value !== null}
|
||||
<InputTextarea
|
||||
id="feedback"
|
||||
placeholder="Your message here"
|
||||
@@ -26,5 +26,5 @@
|
||||
id="email"
|
||||
bind:value={$feedbackData.email}
|
||||
placeholder="Enter email" />
|
||||
</FormList>
|
||||
{/if}
|
||||
{/if}
|
||||
</FormList>
|
||||
|
||||
@@ -72,5 +72,6 @@ export { default as FakeModal } from './fakeModal.svelte';
|
||||
export { default as RadioBoxes } from './radioBoxes.svelte';
|
||||
export { default as ModalWrapper } from './modalWrapper.svelte';
|
||||
export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as EmptyCardImageCloud } from './emptyCardImageCloud.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
|
||||
@@ -121,11 +121,6 @@
|
||||
--p-drop-bg-color: var(--color-neutral-105);
|
||||
--p-drop-border-color: var(--color-neutral-85);
|
||||
|
||||
body.theme-light & {
|
||||
--p-drop-bg-color: var(--color-neutral-0);
|
||||
--p-drop-border-color: var(--color-neutral-10);
|
||||
}
|
||||
|
||||
inset-inline-start: -0.625rem;
|
||||
inset-block-end: calc(100% + 0.625rem);
|
||||
background-color: hsl(var(--p-drop-bg-color));
|
||||
@@ -138,6 +133,11 @@
|
||||
margin-inline: auto;
|
||||
line-height: 1.5;
|
||||
|
||||
body.theme-light & {
|
||||
--p-drop-bg-color: var(--color-neutral-0);
|
||||
--p-drop-border-color: var(--color-neutral-10);
|
||||
}
|
||||
|
||||
.tippy-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
export let type: 'success' | 'warning' | 'error' | 'info' | 'neutral' = 'warning';
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
<p
|
||||
class="helper u-margin-block-start-8"
|
||||
class="helper u-margin-block-start-8 {classes}"
|
||||
class:u-color-text-info={type === 'info'}
|
||||
class:u-color-text-danger={type === 'error'}
|
||||
class:u-color-text-success={type === 'success'}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { DropList } from '$lib/components';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { Label } from '.';
|
||||
import { Helper, Label } from '.';
|
||||
|
||||
/* eslint no-undef: "off" */
|
||||
type Option = $$Generic<{
|
||||
@@ -36,6 +36,7 @@
|
||||
let element: HTMLInputElement;
|
||||
let hasFocus = false;
|
||||
let selected: number = null;
|
||||
let error: string;
|
||||
|
||||
$: if (!hasFocus) {
|
||||
selected = null;
|
||||
@@ -105,7 +106,18 @@
|
||||
dispatch('reset');
|
||||
}
|
||||
|
||||
function handleInvalid(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
error = element.validationMessage;
|
||||
}
|
||||
|
||||
$: showClearBtn = (hasFocus && search) || value;
|
||||
$: if (value || search) error = null;
|
||||
</script>
|
||||
|
||||
<li
|
||||
@@ -147,6 +159,7 @@
|
||||
bind:this={element}
|
||||
on:focus={() => (hasFocus = true)}
|
||||
on:click={() => (hasFocus = true)}
|
||||
on:invalid={handleInvalid}
|
||||
on:input={handleInput}
|
||||
on:keydown={handleKeydown} />
|
||||
{/if}
|
||||
@@ -205,6 +218,9 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
{#if error}
|
||||
<Helper class="u-position-relative" type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -124,6 +124,11 @@
|
||||
{/if}
|
||||
<DropList show={$feedback.show} scrollable on:blur={toggleFeedback}>
|
||||
<button class="button is-small is-text" on:click={toggleFeedback}>
|
||||
{#if $feedback.notification}
|
||||
<span
|
||||
class="notification u-position-absolute u-inset-block-start-8 u-inset-inline-end-8"
|
||||
></span>
|
||||
{/if}
|
||||
<span class="text">Feedback</span>
|
||||
</button>
|
||||
<svelte:fragment slot="other">
|
||||
@@ -179,7 +184,11 @@
|
||||
transition:slideFade|global={{ duration: 150 }}>
|
||||
{#if $organizationList?.total}
|
||||
<section class="drop-section u-overflow-y-auto u-max-height-200">
|
||||
<ul class="drop-list">
|
||||
<ul
|
||||
class="drop-list"
|
||||
data-sveltekit-preload-data={$page.params.organization
|
||||
? false
|
||||
: 'hover'}>
|
||||
{#each $organizationList.teams as org}
|
||||
<DropListLink
|
||||
href={`${base}/console/organization-${org.$id}`}
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
<svelte:component this={component} />
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="form-footer">
|
||||
<div class="u-z-index-20 form-footer">
|
||||
<div class="u-flex u-main-end u-gap-12">
|
||||
{#if !isLastStep && currentStep?.optional}
|
||||
<Button text on:click={() => dispatch('finish')}>
|
||||
|
||||
@@ -18,8 +18,23 @@
|
||||
export let href: $$Props['href'] = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
if (confirmExit) {
|
||||
dispatch('exit');
|
||||
} else {
|
||||
trackEvent('wizard_exit', {
|
||||
from: 'escape'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<header class="wizard-secondary-header">
|
||||
<div class="u-flex u-main-space-between u-gap-32 u-cross-center">
|
||||
<Heading size={5} tag="h1"><slot /></Heading>
|
||||
|
||||
@@ -27,8 +27,11 @@ import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte
|
||||
import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte';
|
||||
import LimitReached from '$lib/components/billing/alerts/limitReached.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte';
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { sizeToBytes, type Size } from '$lib/helpers/sizeConvertion';
|
||||
import { user } from './user';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export type Tier = 'tier-0' | 'tier-1' | 'tier-2';
|
||||
|
||||
@@ -51,6 +54,28 @@ export function tierToPlan(tier: Tier) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getNextTier(tier: Tier) {
|
||||
switch (tier) {
|
||||
case BillingPlan.FREE:
|
||||
return BillingPlan.PRO;
|
||||
case BillingPlan.PRO:
|
||||
return BillingPlan.SCALE;
|
||||
default:
|
||||
return BillingPlan.PRO;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviousTier(tier: Tier) {
|
||||
switch (tier) {
|
||||
case BillingPlan.PRO:
|
||||
return BillingPlan.FREE;
|
||||
case BillingPlan.SCALE:
|
||||
return BillingPlan.PRO;
|
||||
default:
|
||||
return BillingPlan.FREE;
|
||||
}
|
||||
}
|
||||
|
||||
export type PlanServices =
|
||||
| 'bandwidth'
|
||||
| 'bandwidthAddon'
|
||||
@@ -366,6 +391,30 @@ export async function checkForMissingPaymentMethod() {
|
||||
}
|
||||
}
|
||||
|
||||
// Display upgrade banner for new users after 1 week for 30 days
|
||||
export async function checkForNewDevUpgradePro(org: Organization) {
|
||||
if (org?.billingPlan !== BillingPlan.FREE || !browser) return;
|
||||
|
||||
const orgs = await sdk.forConsole.billing.listOrganization([
|
||||
Query.notEqual('billingPlan', BillingPlan.FREE)
|
||||
]);
|
||||
if (orgs?.total) return;
|
||||
|
||||
const now = new Date().getTime();
|
||||
const account = get(user);
|
||||
const accountCreated = new Date(account.$createdAt).getTime();
|
||||
if (now - accountCreated < 1000 * 60 * 60 * 24 * 7) return;
|
||||
const isDismissed = !!localStorage.getItem('newDevUpgradePro');
|
||||
if (isDismissed) return;
|
||||
if (now - accountCreated < 1000 * 60 * 60 * 24 * 37) {
|
||||
headerAlert.add({
|
||||
id: 'newDevUpgradePro',
|
||||
component: newDevUpgradePro,
|
||||
show: true,
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
export const upgradeURL = derived(
|
||||
page,
|
||||
($page) => `${base}/console/organization-${$page.data?.organization?.$id}/change-plan`
|
||||
|
||||
@@ -76,10 +76,10 @@ campaigns
|
||||
data: {
|
||||
reviews: [
|
||||
{
|
||||
name: 'Marius Bloki',
|
||||
desc: 'CTO // mySHOEFITTER',
|
||||
img: 'js_mastery_reviewer_1.png',
|
||||
review: 'Appwrite has been a tremendous asset in implementing our IT infrastructure. Not only is the software an absolute game-changer, but the team is always there when you need them.'
|
||||
name: 'Adrian Hajdin',
|
||||
desc: 'Founder & CEO at JavaScript Mastery',
|
||||
img: 'js_mastery_reviewer_2.webp',
|
||||
review: 'Appwrite is the fastest-to-implement open-source BaaS with a growing community and a practical suite of products and APIs'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ export type FeedbackOption = {
|
||||
export const feedbackOptions: FeedbackOption[] = [
|
||||
{
|
||||
type: 'general',
|
||||
title: 'How can we improve?',
|
||||
desc: 'Your feedback is important to us. Please be honest and tell us what you think.',
|
||||
title: 'Help us improve Appwrite',
|
||||
desc: 'Appwrite evolves with your input. Share your thoughts and help us improve Appwrite.',
|
||||
component: FeedbackGeneral
|
||||
},
|
||||
{
|
||||
type: 'nps',
|
||||
title: 'How are we doing?',
|
||||
desc: "At Appwrite, we value the feedback of our users. That means you! We'd love to hear what you think.",
|
||||
title: 'Help us improve Appwrite',
|
||||
desc: 'Appwrite evolves with your input. Share your thoughts and help us improve Appwrite. If you would like to be contacted regarding your feedback, please share your contact details below.',
|
||||
component: FeedbackNps
|
||||
}
|
||||
];
|
||||
@@ -49,7 +49,7 @@ function createFeedbackDataStore() {
|
||||
message: '',
|
||||
name: '',
|
||||
email: '',
|
||||
value: 0
|
||||
value: null
|
||||
});
|
||||
return {
|
||||
set,
|
||||
@@ -60,7 +60,7 @@ function createFeedbackDataStore() {
|
||||
feedbackData.message = '';
|
||||
feedbackData.name = '';
|
||||
feedbackData.email = '';
|
||||
feedbackData.value = 0;
|
||||
feedbackData.value = null;
|
||||
return feedbackData;
|
||||
});
|
||||
}
|
||||
@@ -71,8 +71,8 @@ export const feedbackData = createFeedbackDataStore();
|
||||
|
||||
function createFeedbackStore() {
|
||||
const { subscribe, update } = writable<Feedback>({
|
||||
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0,
|
||||
visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) : 0,
|
||||
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) ?? 0 : 0,
|
||||
visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) ?? 0 : 0,
|
||||
notification: false,
|
||||
type: 'general',
|
||||
show: false
|
||||
|
||||
@@ -70,7 +70,13 @@
|
||||
'/hackathon',
|
||||
'/mfa'
|
||||
];
|
||||
const acceptedAuthenticatedRoutes = ['/console', '/invite', '/card', '/hackathon'];
|
||||
const acceptedAuthenticatedRoutes = [
|
||||
'/console',
|
||||
'/invite',
|
||||
'/card',
|
||||
'/hackathon',
|
||||
'/recover'
|
||||
];
|
||||
|
||||
const pathname = $page.url.pathname;
|
||||
const user = $page.data.account as Models.User<Record<string, string>>;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
checkForMarkedForDeletion,
|
||||
checkForMandate,
|
||||
checkForMissingPaymentMethod,
|
||||
checkForNewDevUpgradePro,
|
||||
plansInfo
|
||||
} from '$lib/stores/billing';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -235,7 +236,9 @@
|
||||
let isOpen = false;
|
||||
onMount(async () => {
|
||||
loading.set(false);
|
||||
|
||||
if (!localStorage.getItem('feedbackElapsed')) {
|
||||
localStorage.setItem('feedbackElapsed', '0');
|
||||
}
|
||||
setInterval(() => {
|
||||
checkForFeedback(INTERVAL);
|
||||
}, INTERVAL);
|
||||
@@ -266,10 +269,13 @@
|
||||
if (isCloud) {
|
||||
await checkForUsageLimit(org);
|
||||
checkForMarkedForDeletion(org);
|
||||
await checkForNewDevUpgradePro(org);
|
||||
|
||||
if (org?.billingPlan !== BillingPlan.FREE) {
|
||||
await paymentExpired(org);
|
||||
await checkPaymentAuthorizationRequired(org);
|
||||
await checkForMandate(org);
|
||||
|
||||
if ($plansInfo.get(org.billingPlan)?.trialDays) {
|
||||
calculateTrialDay(org);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
|
||||
let previousPage: string = `${base}/console`;
|
||||
let showExitModal = false;
|
||||
let canSelectOrg = true;
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
if (from?.url?.pathname) {
|
||||
@@ -47,6 +48,7 @@
|
||||
|
||||
let selectedOrgId: string = null;
|
||||
let formComponent: Form;
|
||||
let couponForm: Form;
|
||||
let isSubmitting = writable(false);
|
||||
let methods: PaymentList;
|
||||
let paymentMethodId: string;
|
||||
@@ -74,6 +76,10 @@
|
||||
if (!$organizationList?.total || campaign?.onlyNewOrgs) {
|
||||
selectedOrgId = newOrgId;
|
||||
}
|
||||
if ($page.url.searchParams.has('org')) {
|
||||
selectedOrgId = $page.url.searchParams.get('org');
|
||||
canSelectOrg = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
@@ -87,6 +93,7 @@
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!couponForm.checkValidity()) return;
|
||||
try {
|
||||
let org: Organization;
|
||||
// Create new org
|
||||
@@ -181,19 +188,7 @@
|
||||
(team) => team.$id === selectedOrgId
|
||||
) as Organization;
|
||||
|
||||
$: isButtonDisabled =
|
||||
checkButtonDisabled(selectedOrgId, name, paymentMethodId) ||
|
||||
couponData?.status !== 'active';
|
||||
|
||||
function checkButtonDisabled(id: string | null, name: string | null, method: string | null) {
|
||||
if (id === newOrgId) {
|
||||
return !name || !method;
|
||||
} else if (id && selectedOrg?.billingPlan === BillingPlan.PRO) {
|
||||
return !selectedOrg?.paymentMethodId;
|
||||
} else if (id) {
|
||||
return !method;
|
||||
} else return true;
|
||||
}
|
||||
$: campaign = campaigns.get(data?.couponData?.campaign ?? data?.campaign);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -207,7 +202,7 @@
|
||||
<WizardSecondaryContent>
|
||||
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
|
||||
<FormList>
|
||||
{#if $organizationList?.total && !campaign?.onlyNewOrgs}
|
||||
{#if $organizationList?.total && !campaign?.onlyNewOrgs && canSelectOrg}
|
||||
<InputSelect
|
||||
bind:value={selectedOrgId}
|
||||
label="Select organization"
|
||||
@@ -237,10 +232,11 @@
|
||||
{/if}
|
||||
</FormList>
|
||||
</Form>
|
||||
<Form onSubmit={addCoupon}>
|
||||
<Form bind:this={couponForm} onSubmit={addCoupon}>
|
||||
<FormList>
|
||||
{#if !data?.couponData?.code && selectedOrgId}
|
||||
<InputText
|
||||
required
|
||||
disabled={!!couponData?.credits}
|
||||
bind:value={coupon}
|
||||
placeholder="Enter coupon code"
|
||||
@@ -319,8 +315,12 @@
|
||||
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting || isButtonDisabled}>
|
||||
on:click={() => {
|
||||
if (formComponent.checkValidity() && couponForm.checkValidity()) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
disabled={$isSubmitting}>
|
||||
{#if $isSubmitting}
|
||||
<span class="loader is-small is-transparent u-line-height-1-5" aria-hidden="true" />
|
||||
{/if}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { selectedInvoice, showRetryModal } from './store';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
let showDropdown = [];
|
||||
let showFailedError = false;
|
||||
@@ -48,6 +49,7 @@
|
||||
invoiceList = await sdk.forConsole.billing.listInvoices($page.params.organization, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.notEqual('from', $organization.billingCurrentInvoiceDate),
|
||||
Query.orderDesc('$createdAt')
|
||||
]);
|
||||
}
|
||||
@@ -70,7 +72,7 @@
|
||||
payments.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if invoiceList.total - 1 > 0}
|
||||
{#if invoiceList.total > 0}
|
||||
<TableScroll noMargin transparent noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead width={100}>Due Date</TableCellHead>
|
||||
@@ -81,115 +83,108 @@
|
||||
<TableBody>
|
||||
{#each invoiceList?.invoices as invoice, i}
|
||||
{@const status = invoice.status}
|
||||
{#if i !== 0}
|
||||
<TableRow>
|
||||
<TableCellText title="date">
|
||||
{toLocaleDate(invoice.dueAt)}
|
||||
</TableCellText>
|
||||
|
||||
<TableCell title="status">
|
||||
{#if invoice?.lastError}
|
||||
<DropList bind:show={showFailedError}>
|
||||
<Pill
|
||||
danger={status === 'overdue' ||
|
||||
status === 'failed' ||
|
||||
status === 'requires_authentication'}
|
||||
success={status === 'paid' ||
|
||||
status === 'succeeded'}
|
||||
warning={status === 'pending'}
|
||||
on:click={() => (showFailedError = true)}
|
||||
button>
|
||||
{status === 'requires_authentication'
|
||||
? 'failed'
|
||||
: status}
|
||||
</Pill>
|
||||
<svelte:fragment slot="list">
|
||||
<li>
|
||||
The scheduled payment has failed. <Button
|
||||
link
|
||||
on:click={() => {
|
||||
retryPayment(invoice);
|
||||
showFailedError = false;
|
||||
}}>Try again</Button
|
||||
>.
|
||||
</li>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
{:else}
|
||||
<TableRow>
|
||||
<TableCellText title="date">
|
||||
{toLocaleDate(invoice.dueAt)}
|
||||
</TableCellText>
|
||||
<TableCell title="status">
|
||||
{#if invoice?.lastError}
|
||||
<DropList bind:show={showFailedError}>
|
||||
<Pill
|
||||
danger={status === 'overdue' ||
|
||||
status === 'failed' ||
|
||||
status === 'requires_authentication'}
|
||||
success={status === 'paid' || status === 'succeeded'}
|
||||
warning={status === 'pending'}>
|
||||
warning={status === 'pending'}
|
||||
on:click={() => (showFailedError = true)}
|
||||
button>
|
||||
{status === 'requires_authentication'
|
||||
? 'failed'
|
||||
: status}
|
||||
</Pill>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCellText title="due">
|
||||
{formatCurrency(invoice.amount)}
|
||||
</TableCellText>
|
||||
<TableCell showOverflow right>
|
||||
<DropList
|
||||
bind:show={showDropdown[i]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<Button
|
||||
round
|
||||
text
|
||||
noMargin
|
||||
ariaLabel="More options"
|
||||
on:click={() => {
|
||||
showDropdown[i] = !showDropdown[i];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListLink
|
||||
icon="external-link"
|
||||
external
|
||||
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/view`}
|
||||
on:click={() =>
|
||||
(showDropdown[i] = !showDropdown[i])}
|
||||
event="view_invoice">
|
||||
View invoice
|
||||
</DropListLink>
|
||||
<DropListLink
|
||||
icon="download"
|
||||
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/download`}
|
||||
on:click={() => {
|
||||
showDropdown[i] = !showDropdown[i];
|
||||
}}
|
||||
event="download_invoice">
|
||||
Download PDF
|
||||
</DropListLink>
|
||||
{#if status === 'overdue' || status === 'failed'}
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
<li>
|
||||
The scheduled payment has failed. <Button
|
||||
link
|
||||
on:click={() => {
|
||||
retryPayment(invoice);
|
||||
showDropdown[i] = !showDropdown[i];
|
||||
trackEvent(`click_retry_payment`, {
|
||||
from: 'button',
|
||||
source: 'billing_invoice_menu'
|
||||
});
|
||||
}}>
|
||||
Retry payment
|
||||
</DropListItem>
|
||||
{/if}
|
||||
showFailedError = false;
|
||||
}}>Try again</Button
|
||||
>.
|
||||
</li>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{:else}
|
||||
<Pill
|
||||
danger={status === 'overdue' ||
|
||||
status === 'failed' ||
|
||||
status === 'requires_authentication'}
|
||||
success={status === 'paid' || status === 'succeeded'}
|
||||
warning={status === 'pending'}>
|
||||
{status === 'requires_authentication' ? 'failed' : status}
|
||||
</Pill>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCellText title="due">
|
||||
{formatCurrency(invoice.amount)}
|
||||
</TableCellText>
|
||||
<TableCell showOverflow right>
|
||||
<DropList
|
||||
bind:show={showDropdown[i]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<Button
|
||||
round
|
||||
text
|
||||
noMargin
|
||||
ariaLabel="More options"
|
||||
on:click={() => {
|
||||
showDropdown[i] = !showDropdown[i];
|
||||
}}>
|
||||
<span class="icon-dots-horizontal" aria-hidden="true" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<DropListLink
|
||||
icon="external-link"
|
||||
external
|
||||
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/view`}
|
||||
on:click={() => (showDropdown[i] = !showDropdown[i])}
|
||||
event="view_invoice">
|
||||
View invoice
|
||||
</DropListLink>
|
||||
<DropListLink
|
||||
icon="download"
|
||||
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/download`}
|
||||
on:click={() => {
|
||||
showDropdown[i] = !showDropdown[i];
|
||||
}}
|
||||
event="download_invoice">
|
||||
Download PDF
|
||||
</DropListLink>
|
||||
{#if status === 'overdue' || status === 'failed'}
|
||||
<DropListItem
|
||||
icon="refresh"
|
||||
on:click={() => {
|
||||
retryPayment(invoice);
|
||||
showDropdown[i] = !showDropdown[i];
|
||||
trackEvent(`click_retry_payment`, {
|
||||
from: 'button',
|
||||
source: 'billing_invoice_menu'
|
||||
});
|
||||
}}>
|
||||
Retry payment
|
||||
</DropListItem>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total results: {invoiceList?.total - 1 ?? 0}</p>
|
||||
<PaginationInline {limit} bind:offset sum={invoiceList?.total - 1 ?? 0} hidePages />
|
||||
<p class="text">Total results: {invoiceList?.total ?? 0}</p>
|
||||
<PaginationInline {limit} bind:offset sum={invoiceList?.total ?? 0} hidePages />
|
||||
</div>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
onMount(async () => {
|
||||
const invoices = await sdk.forConsole.billing.listInvoices($organization.$id, [
|
||||
Query.limit(1),
|
||||
Query.orderDesc('$createdAt')
|
||||
Query.equal('from', $organization.billingCurrentInvoiceDate)
|
||||
]);
|
||||
currentInvoice = invoices.invoices[0];
|
||||
const members = await sdk.forConsole.teams.listMemberships($organization.$id, []);
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
})
|
||||
});
|
||||
|
||||
await goto(`/console/organization-${$organization.$id}`);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
isHtml: true,
|
||||
@@ -242,12 +243,7 @@
|
||||
$: if (billingPlan === BillingPlan.PRO) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
$: isButtonDisabled =
|
||||
$organization.billingPlan === billingPlan
|
||||
? true
|
||||
: isUpgrade
|
||||
? !paymentMethodId
|
||||
: !feedbackDowngradeReason;
|
||||
$: isButtonDisabled = $organization.billingPlan === billingPlan;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -255,8 +251,9 @@
|
||||
</svelte:head>
|
||||
|
||||
<WizardSecondaryContainer bind:showExitModal href={previousPage}>
|
||||
<WizardSecondaryHeader confirmExit on:exit={() => (showExitModal = true)}
|
||||
>Change plan</WizardSecondaryHeader>
|
||||
<WizardSecondaryHeader confirmExit on:exit={() => (showExitModal = true)}>
|
||||
Change plan
|
||||
</WizardSecondaryHeader>
|
||||
<WizardSecondaryContent>
|
||||
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
|
||||
<Label class="label u-margin-block-start-16">Select plan</Label>
|
||||
@@ -276,6 +273,7 @@
|
||||
{/if}
|
||||
<ul
|
||||
class="u-flex u-gap-16 u-margin-block-start-8"
|
||||
class:u-margin-block-start-16={anyOrgFree && billingPlan === BillingPlan.PRO}
|
||||
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard
|
||||
@@ -351,17 +349,17 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !isUpgrade && billingPlan === BillingPlan.FREE && $organization.billingPlan !== BillingPlan.FREE}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<FormList class="u-margin-block-start-24">
|
||||
<InputSelect
|
||||
id="reason"
|
||||
label="What made you decide to change your plan?"
|
||||
label="Reason for plan change"
|
||||
placeholder="Select one"
|
||||
required
|
||||
options={feedbackDowngradeOptions}
|
||||
bind:value={feedbackDowngradeReason} />
|
||||
<InputTextarea
|
||||
id="comment"
|
||||
label="Your feedback here"
|
||||
label="If you need to elaborate, please do so here"
|
||||
placeholder="Enter feedback"
|
||||
bind:value={feedbackMessage} />
|
||||
</FormList>
|
||||
@@ -375,7 +373,7 @@
|
||||
bind:couponData
|
||||
bind:billingBudget />
|
||||
{:else}
|
||||
<PlanComparisonBox />
|
||||
<PlanComparisonBox downgrade={isDowngrade} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</WizardSecondaryContent>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
aria-hidden="true" />
|
||||
</Heading>
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div data-sveltekit-preload-data="false" slot="list">
|
||||
{#each $organizationList.teams as org}
|
||||
<DropListLink
|
||||
href={`${base}/console/organization-${org.$id}`}
|
||||
@@ -135,7 +135,7 @@
|
||||
{org.name}
|
||||
</DropListLink>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</div>
|
||||
<svelte:fragment slot="other">
|
||||
<section class="drop-section">
|
||||
<ul class="drop-list">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<CardGrid>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">Download DPA document</Heading>
|
||||
<Heading tag="h6" size="7">DPA</Heading>
|
||||
</div>
|
||||
<p class="text">
|
||||
After downloading, have the DPA signed by your organization's compliance authority, such as
|
||||
@@ -27,7 +27,7 @@
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<h6>
|
||||
<b>Data Processing Agreement (DPA) document</b>
|
||||
<b>Data Processing Agreement (DPA)</b>
|
||||
</h6>
|
||||
<p class="text u-margin-block-start-8">
|
||||
The DPA is a legal document that describes the roles and responsibilities of
|
||||
@@ -35,8 +35,7 @@
|
||||
class="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://appwrite.io/docs/advanced/security/gdpr#dpa"
|
||||
>Learn more about the DPA</a
|
||||
href="https://appwrite.io/docs/advanced/security/gdpr#dpa">Learn more</a
|
||||
>.
|
||||
</p>
|
||||
<Button
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import EmailSignature from './emailSignature.svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type {
|
||||
SmsTemplateLocale,
|
||||
SmsTemplateType,
|
||||
@@ -58,7 +60,6 @@
|
||||
EmailTemplateLocale
|
||||
} from '@appwrite.io/console';
|
||||
import Email2FaTemplate from './email2FATemplate.svelte';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
@@ -132,18 +133,6 @@
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $organization.billingPlan === BillingPlan.FREE}
|
||||
<Alert
|
||||
buttons={[
|
||||
{
|
||||
slot: 'Upgrade plan',
|
||||
href: `${base}/console/organization-${$organization.$id}/billing`
|
||||
}
|
||||
]}>
|
||||
All emails sent using the {tierToPlan(BillingPlan.FREE).name} plan will include attribution
|
||||
to Appwrite in the signature. To send attribution-free emails, upgrade your plan.
|
||||
</Alert>
|
||||
{/if}
|
||||
<Collapsible>
|
||||
<CollapsibleItem
|
||||
bind:open={emailVerificationOpen}
|
||||
@@ -257,4 +246,7 @@
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
</CardGrid>-->
|
||||
{#if isCloud && $organization?.billingPlan === BillingPlan.FREE}
|
||||
<EmailSignature />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, EmptyCardImageCloud, Heading } from '$lib/components';
|
||||
import { app } from '$lib/stores/app';
|
||||
import EmailDark from './email-footer-dark.png';
|
||||
import EmailLight from './email-footer-light.png';
|
||||
import EmailMobileDark from './email-footer-mobile-dark.png';
|
||||
import EmailMobileLight from './email-footer-mobile-light.png';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading size="7" tag="h3">Email signature</Heading>
|
||||
<p class="text">Enable or disable Appwrite branding in your email template signature.</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<EmptyCardImageCloud source="email_signature_card" let:nextTier noAspectRatio>
|
||||
<svelte:fragment slot="image">
|
||||
<div class=" is-only-mobile u-width-full-line u-height-100-percent">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={EmailMobileDark}
|
||||
class="u-image-object-fit-cover u-only-dark u-width-full-line u-height-100-percent"
|
||||
alt="Email Signature Example" />
|
||||
{:else}
|
||||
<img
|
||||
src={EmailMobileLight}
|
||||
class="u-image-object-fit-cover u-only-light u-width-full-line u-height-100-percent"
|
||||
alt="Email Signature Example" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="is-not-mobile u-width-full-line u-height-100-percent">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={EmailDark}
|
||||
width="266"
|
||||
height="171"
|
||||
class="u-image-object-fit-contain u-block u-only-dark u-width-full-line u-height-100-percent"
|
||||
style:object-position="top"
|
||||
alt="Email Signature Example" />
|
||||
{:else}
|
||||
<img
|
||||
src={EmailLight}
|
||||
width="266"
|
||||
height="171"
|
||||
class="u-image-object-fit-contain u-only-light u-width-full-line u-height-100-percent"
|
||||
style:object-position="top"
|
||||
alt="Email Signature Example" />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">Upgrade to edit email signature</svelte:fragment>
|
||||
Upgrade to a {nextTier} plan to enable or disable Appwrite branding in your emails.
|
||||
</EmptyCardImageCloud>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -16,7 +16,7 @@
|
||||
style="color:hsl(var(--color-danger-100));" />Message failed
|
||||
</p>
|
||||
<p>
|
||||
The message has been sent with errors. Please refer to the logs below for more
|
||||
The message has been processed with errors. Please refer to the logs below for more
|
||||
information.
|
||||
</p>
|
||||
<div style="max-inline-size: 524px">
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { isSupportOnline, supportData } from './wizard/support/store';
|
||||
import Step1 from './wizard/support/step1.svelte';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { VARS } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import {
|
||||
localeTimezoneName,
|
||||
utcHourToLocaleHour,
|
||||
utcWeekDayToLocaleWeekDay,
|
||||
type WeekDay
|
||||
} from '$lib/helpers/date';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { VARS } from '$lib/system';
|
||||
import { onDestroy } from 'svelte';
|
||||
import Step1 from './wizard/support/step1.svelte';
|
||||
import { isSupportOnline, supportData } from './wizard/support/store';
|
||||
|
||||
onDestroy(() => {
|
||||
$supportData = {
|
||||
@@ -47,7 +47,7 @@
|
||||
customFields: [
|
||||
{ id: '41612', value: $supportData.category },
|
||||
{ id: '48493', value: $user?.name ?? '' },
|
||||
{ id: '48492', value: $organization.$id ?? '' },
|
||||
{ id: '48492', value: $organization?.$id ?? '' },
|
||||
{ id: '48491', value: $supportData?.project ?? '' },
|
||||
{ id: '48490', value: $user?.$id ?? '' }
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Feedback submitted successfully'
|
||||
message: 'Thank you for your feedback'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 988 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1051" height="1271" viewBox="0 0 1051 1271" fill="none">
|
||||
<g filter="url(#filter0_f_2147_2948)">
|
||||
<path d="M712.439 968.914C643.17 1007.71 645.862 391.902 460.132 633.57C274.401 875.238 273.104 347.057 342.373 308.256C411.642 269.454 550.638 385.893 652.829 568.329C755.02 750.765 781.708 930.113 712.439 968.914Z" fill="url(#paint0_radial_2147_2948)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_2147_2948" x="0.219421" y="0.797729" width="1050.74" height="1269.88" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="150" result="effect1_foregroundBlur_2147_2948"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_2147_2948" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(496.859 629.666) rotate(137.143) scale(145.953 366.505)">
|
||||
<stop stop-color="#E7F8F7"/>
|
||||
<stop offset="1" stop-color="#85DBD8"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 858 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1283" height="1278" viewBox="0 0 1283 1278" fill="none">
|
||||
<g filter="url(#filter0_f_2147_2911)">
|
||||
<path d="M422.869 374.715C278.243 495.214 258.684 710.14 379.183 854.766C499.683 999.392 714.609 1018.95 859.235 898.452C930.123 839.389 651.054 705.413 667.842 602.647C685.305 495.749 1036.75 504.514 975.315 430.777C854.815 286.15 567.495 254.215 422.869 374.715Z" fill="url(#paint0_radial_2147_2911)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_2147_2911" x="0.192719" y="0.607666" width="1282.32" height="1276.83" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="150" result="effect1_foregroundBlur_2147_2911"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_2147_2911" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(641.052 636.583) rotate(140.2) scale(340.85 340.851)">
|
||||
<stop offset="0.281696" stop-color="#FE9567"/>
|
||||
<stop offset="0.59375" stop-color="#FD366E"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -24,7 +24,8 @@ export async function createProProject(page: Page): Promise<Metadata> {
|
||||
await page.locator('id=plan').selectOption('tier-1');
|
||||
await page.getByRole('button', { name: 'get started' }).click();
|
||||
await page.waitForURL('/console/create-organization**');
|
||||
await page.getByRole('button', { name: 'Add' }).first().click();
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
await page.getByRole('button', { name: 'add' }).first().click();
|
||||
await enterCreditCard(page);
|
||||
// skip members
|
||||
await page.getByRole('button', { name: 'create organization' }).click();
|
||||
|
||||