Merge branch 'main' of https://github.com/appwrite/console into 1.6.x

This commit is contained in:
Torsten Dittmann
2024-07-15 13:39:22 +02:00
53 changed files with 1263 additions and 865 deletions
+1 -6
View File
@@ -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:
+1 -1
View File
@@ -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:
+639 -625
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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",
+3 -1
View File
@@ -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
+19
View File
@@ -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>
+7 -2
View File
@@ -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}
+1 -1
View File
@@ -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) {
+2 -2
View File
@@ -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>
+5 -5
View File
@@ -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>
+1
View File
@@ -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';
+5 -5
View File
@@ -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;
}
+3 -1
View File
@@ -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>
+10 -1
View File
@@ -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}`}
+1 -1
View File
@@ -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>
+49
View File
@@ -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`
+4 -4
View File
@@ -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'
}
]
}
+8 -8
View File
@@ -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
+7 -1
View File
@@ -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>>;
+7 -1
View File
@@ -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);
}
+17 -17
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

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">
+11 -11
View File
@@ -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({
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.
+2 -1
View File
@@ -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();