Merge branch '1.5.x' into fix-update-exceed-modal-copy

This commit is contained in:
Wess Cope
2024-02-15 15:01:19 -05:00
committed by GitHub
153 changed files with 2541 additions and 2446 deletions
+1 -1
View File
@@ -2,4 +2,4 @@ VITE_APPWRITE_ENDPOINT=http://localhost/v1
VITE_APPWRITE_GROWTH_ENDPOINT=
VITE_GA_PROJECT=
VITE_CONSOLE_MODE=self-hosted
VITE_STRIPE_PUBLIC_KEY=
VITE_STRIPE_PUBLIC_KEY=
+4
View File
@@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
+1129 -897
View File
File diff suppressed because it is too large Load Diff
+14 -15
View File
@@ -10,8 +10,8 @@
"sync": "svelte-kit sync",
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin prettier-plugin-svelte . && eslint .",
"format": "prettier --ignore-path .gitignore --write --plugin prettier-plugin-svelte .",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:ui": "vitest --ui",
"test:watch": "vitest watch",
@@ -19,8 +19,8 @@
},
"dependencies": {
"@appwrite.io/console": "^0.6.0-rc.9",
"@appwrite.io/pink": "0.2.0",
"@appwrite.io/pink-icons": "0.2.0",
"@appwrite.io/pink": "0.4.0",
"@appwrite.io/pink-icons": "0.4.0",
"@popperjs/core": "^2.11.8",
"@sentry/svelte": "^7.66.0",
"@sentry/tracing": "^7.66.0",
@@ -37,16 +37,15 @@
"pretty-bytes": "^6.1.1",
"prismjs": "^1.29.0",
"svelte-confetti": "^1.3.0",
"tippy.js": "^6.3.7",
"web-vitals": "^3.4.0"
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.61.2",
"@playwright/test": "^1.37.1",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.24.0",
"@sveltejs/vite-plugin-svelte": "^2.4.5",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.3.4",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@testing-library/dom": "^9.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^4.0.3",
@@ -55,24 +54,24 @@
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@vitest/ui": "^0.29.7",
"@vitest/ui": "^1.2.1",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.33.0",
"jsdom": "^22.1.0",
"kleur": "^4.1.5",
"prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3",
"prettier": "^3.2.2",
"prettier-plugin-svelte": "^3.1.2",
"sass": "^1.66.1",
"svelte": "^4.2.0",
"svelte": "^4.2.9",
"svelte-check": "^3.5.1",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^5.0.4",
"svelte-sequential-preprocessor": "^2.0.1",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vitest": "^0.29.7"
"vite": "^5.0.11",
"vitest": "^1.2.1"
},
"type": "module"
}
+1 -3
View File
@@ -1,4 +1,2 @@
/// <reference types="@sveltejs/kit" />
interface Window {
VERCEL_ANALYTICS_ID: string | false;
}
interface Window {}
+1
View File
@@ -265,6 +265,7 @@ export enum Submit {
PaymentMethodCreate = 'submit_payment_method_create',
PaymentMethodUpdate = 'submit_payment_method_update',
PaymentMethodDelete = 'submit_payment_method_delete',
RetryPayment = 'submit_retry_payment',
BillingAddressCreate = 'submit_billing_address_create',
BillingAddressUpdate = 'submit_billing_address_update',
BillingAddressDelete = 'submit_billing_address_delete',
@@ -1,13 +0,0 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
</script>
<HeaderAlert title="You are limited to one free organization per account">
All but one organization will be automatically upgraded to a Pro plan on <b>31 January 2024</b>.
You can add a payment method or transfer projects from your settings.
<svelte:fragment slot="buttons">
<Button secondary href="/console/account/organizations">View organizations</Button>
</svelte:fragment>
</HeaderAlert>
@@ -1,5 +1,6 @@
<script lang="ts">
import { Button, FormList, InputText } from '$lib/elements/forms';
import { formatCurrency } from '$lib/helpers/numbers';
import type { Coupon } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { createEventDispatcher } from 'svelte';
@@ -55,7 +56,7 @@
<div>
<span class="icon-exclamation-circle u-color-text-danger" />
<span>
{couponData.code} is not a valid promo code
{couponData.code.toUpperCase()} is not a valid promo code
</span>
</div>
{:else if couponData?.status === 'active'}
@@ -64,11 +65,13 @@
<span class="icon-tag u-color-text-success" />
<slot data={couponData}>
<span>
{couponData.code} applied (-${couponData.credits})
{couponData.code.toUpperCase()} applied (-{formatCurrency(
couponData.credits
)})
</span>
</slot>
</div>
<Button text on:click={removeCoupon}>Remove</Button>
<Button round text on:click={removeCoupon}><span class="icon-x"></span></Button>
</div>
{/if}
</FormList>
+48 -6
View File
@@ -1,12 +1,18 @@
<script lang="ts">
import { FormList, InputText } from '$lib/elements/forms';
import { FormList, InputChoice, InputText } from '$lib/elements/forms';
import { onDestroy, onMount } from 'svelte';
import { CreditCardBrandImage, RadioBoxes } from '..';
import { unmountPaymentElement } from '$lib/stores/stripe';
import { Pill } from '$lib/elements';
export let methods: Record<string, unknown>[];
export let group: string;
export let name: string;
export let defaultMethod: string = null;
export let backupMethod: string = null;
export let disabledCondition: string = null;
export let setAsDefault = false;
export let showSetAsDefault = false;
let element: HTMLDivElement;
let loader: HTMLDivElement;
@@ -40,15 +46,43 @@
$: if (element) {
observer.observe(element, { childList: true });
}
//Set setAsDefault as false when group changes
$: if (group || group === null) {
setAsDefault = false;
}
</script>
<RadioBoxes elements={methods} total={methods?.length} variableName="$id" name="payment" bind:group>
<RadioBoxes
elements={methods}
total={methods?.length}
variableName="$id"
name="payment"
bind:group
{disabledCondition}>
<svelte:fragment slot="element" let:element>
<slot {element}>
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
<span>
<span class="u-capitalize">{element.brand}</span> ending in {element.last4}</span>
<CreditCardBrandImage brand={element.brand?.toString()} />
<span class="u-flex u-gap-16 u-flex-vertical">
<span class="u-flex u-gap-16">
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
<span>
<span class="u-capitalize">{element.brand}</span> ending in {element.last4}</span>
<CreditCardBrandImage brand={element.brand?.toString()} />
</span>
{#if element.$id === backupMethod}
<Pill>Backup</Pill>
{:else if element.$id === defaultMethod}
<Pill>Default</Pill>
{/if}
</span>
{#if !!defaultMethod && element.$id !== defaultMethod && group === element.$id && showSetAsDefault && element.$id !== backupMethod}
<ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
</ul>
{/if}
</span>
</slot>
</svelte:fragment>
@@ -73,6 +107,14 @@
<!-- Stripe will create form elements here -->
</div>
</div>
{#if showSetAsDefault}
<ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
</ul>
{/if}
</FormList>
</RadioBoxes>
+2 -1
View File
@@ -3,6 +3,7 @@
export let danger = false;
export let hideOverflow = false;
export let hideFooter = false;
</script>
<Card {danger}>
@@ -14,7 +15,7 @@
<slot name="aside" />
</div>
</div>
{#if $$slots.actions}
{#if $$slots.actions && !hideFooter}
<div class="common-section card-separator u-flex u-main-end">
<slot name="actions" />
</div>
+13 -11
View File
@@ -110,17 +110,19 @@
</div>
</header>
<div class="modal-content">
{#if error}
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
{/if}
<slot />
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
{#if error}
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
{/if}
<slot />
</div>
</div>
{#if $$slots.footer}
+15 -13
View File
@@ -71,19 +71,21 @@
</p>
</header>
<div class="modal-content">
{#if error}
<div bind:this={alert}>
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
</div>
{/if}
<slot />
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
{#if error}
<div bind:this={alert}>
<Alert
dismissible
type="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
</div>
{/if}
<slot />
</div>
</div>
{#if $$slots.footer}
+4 -2
View File
@@ -41,8 +41,10 @@
</slot>
</p>
</header>
<div class="modal-content mk-content">
<slot />
<div class="modal-content">
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
<slot />
</div>
</div>
</div>
</div>
+3 -2
View File
@@ -70,12 +70,13 @@
<dialog
class="modal"
class:u-hide={!show}
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-separate-header={headerDivider}
{style}
bind:this={dialog}
on:cancel|preventDefault>
on:cancel|preventDefault
{style}>
{#if show}
<slot close={closeModal} />
{/if}
+4 -4
View File
@@ -13,7 +13,7 @@
{#if total}
{#each elements as element}
{@const value = element[variableName]?.toString()}
<div class="box">
<ul class="box" data-private>
<InputRadio
id={`${name}-${value}`}
{value}
@@ -22,11 +22,11 @@
disabled={disabledCondition ? value === disabledCondition : false}>
<slot name="element" {element} />
</InputRadio>
</div>
</ul>
{/each}
{/if}
<div class="box">
<ul class="box">
{#if total}
<InputRadio id="payment-method" value={null} {name} bind:group>
<slot name="new">
@@ -37,5 +37,5 @@
{#if group === null}
<slot />
{/if}
</div>
</ul>
</div>
+12 -1
View File
@@ -11,6 +11,8 @@
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { trackEvent } from '$lib/actions/analytics';
export let show = false;
@@ -43,7 +45,16 @@
{/if}
</div>
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button fullWidth href="https://appwrite.io/pricing" external>
<Button
fullWidth
external
on:click={() => {
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'support_menu'
});
}}>
<span class="text">Get Premium support</span>
</Button>
{:else}
+1 -339
View File
@@ -3,6 +3,7 @@ export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Dependencies {
FACTORS = 'dependency:factors',
CREDIT = 'dependency:credit',
INVOICES = 'dependency:invoices',
ADDRESS = 'dependency:address',
@@ -353,345 +354,6 @@ export const eventServices: Array<EventService> = [
}
];
export const usageRates = {
'tier-0': [
{
id: 'members',
resource: 'Organization members',
amount: 1,
unit: '',
rate: '$20/member'
},
{ id: 'bandwith', resource: 'Bandwidth', amount: 10, unit: 'GB', rate: '$0.04/GB' },
{ id: 'storage', resource: 'Storage', amount: 2, unit: 'GB', rate: '$0.025/GB' },
{
id: 'executions',
resource: 'Function executions',
amount: 750000,
unit: 'executions',
rate: '$2/1M executions'
},
{
id: 'users',
resource: 'Active users',
amount: 200000,
unit: 'AU',
rate: '$0.0012/user'
},
{
id: 'connections',
resource: 'Concurrent connections',
amount: 750,
unit: 'connections',
rate: '$5/1K connections'
}
],
'tier-1': [
{
id: 'members',
resource: 'Organization members',
amount: 'Unlimited',
unit: '',
rate: '$20/member'
},
{ id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' },
{ id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' },
{
id: 'executions',
resource: 'Function executions',
amount: 3500000,
unit: 'executions',
rate: '$2/1M executions'
},
{
id: 'users',
resource: 'Active users',
amount: 200000,
unit: 'AU',
rate: '$0.0012/user'
},
{
id: 'connections',
resource: 'Concurrent connections',
amount: 750,
unit: 'connections',
rate: '$5/1K connections'
}
],
'tier-2': [
{
id: 'members',
resource: 'Organization members',
amount: 'Unlimited',
unit: '',
rate: '$20/member'
},
{ id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' },
{ id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' },
{
id: 'executions',
resource: 'Function executions',
amount: 3500000,
unit: 'executions',
rate: '$2/1M executions'
},
{
id: 'users',
resource: 'Active users',
amount: 200000,
unit: 'AU',
rate: '$0.0012/user'
},
{
id: 'connections',
resource: 'Concurrent connections',
amount: 750,
unit: 'connections',
rate: '$5/1K connections'
}
]
};
//resources: bandwidth, buckets, file size limit, functions, executions, users,teams,logs, members, platforms, webhooks, databases, connections, messages
export const limitRates = {
'tier-0': [
{
id: 'bandwith',
amount: 10,
unit: 'GB'
},
{
id: 'buckets',
amount: 3,
unit: ''
},
{
id: 'file-size-limit',
amount: 1,
unit: 'MB'
},
{
id: 'storage',
amount: 5,
unit: 'GB'
},
{
id: 'functions',
amount: 1,
unit: ''
},
{
id: 'executions',
amount: 750000,
unit: 'executions'
},
{
id: 'users',
amount: 200000,
unit: 'AU'
},
{
id: 'teams',
amount: 1,
unit: ''
},
{
id: 'logs',
amount: 1,
unit: ''
},
{
id: 'members',
amount: 1,
unit: ''
},
{
id: 'platforms',
amount: 1,
unit: ''
},
{
id: 'webhooks',
amount: 1,
unit: ''
},
{
id: 'databases',
amount: 1,
unit: ''
},
{
id: 'connections',
amount: 1,
unit: ''
},
{
id: 'messages',
amount: 1,
unit: ''
}
],
'tier-1': [
{
id: 'bandwith',
amount: 10,
unit: 'GB'
},
{
id: 'buckets',
amount: 1,
unit: ''
},
{
id: 'file-size-limit',
amount: 5,
unit: 'MB'
},
{
id: 'storage',
amount: 5,
unit: 'GB'
},
{
id: 'functions',
amount: 1,
unit: ''
},
{
id: 'executions',
amount: 750000,
unit: 'executions'
},
{
id: 'users',
amount: 200000,
unit: 'AU'
},
{
id: 'teams',
amount: 1,
unit: ''
},
{
id: 'logs',
amount: 1,
unit: ''
},
{
id: 'members',
amount: 1,
unit: ''
},
{
id: 'platforms',
amount: 1,
unit: ''
},
{
id: 'webhooks',
amount: 1,
unit: ''
},
{
id: 'databases',
amount: 1,
unit: ''
},
{
id: 'connections',
amount: 1,
unit: ''
},
{
id: 'messages',
amount: 1,
unit: ''
}
],
'tier-2': [
{
id: 'bandwith',
amount: 10,
unit: 'GB'
},
{
id: 'buckets',
amount: 1,
unit: ''
},
{
id: 'file-size-limit',
amount: 5,
unit: 'MB'
},
{
id: 'storage',
amount: 5,
unit: 'GB'
},
{
id: 'functions',
amount: 1,
unit: ''
},
{
id: 'executions',
amount: 750000,
unit: 'executions'
},
{
id: 'users',
amount: 200000,
unit: 'AU'
},
{
id: 'teams',
amount: 1,
unit: ''
},
{
id: 'logs',
amount: 1,
unit: ''
},
{
id: 'members',
amount: 1,
unit: ''
},
{
id: 'platforms',
amount: 1,
unit: ''
},
{
id: 'webhooks',
amount: 1,
unit: ''
},
{
id: 'databases',
amount: 1,
unit: ''
},
{
id: 'connections',
amount: 1,
unit: ''
},
{
id: 'messages',
amount: 1,
unit: ''
}
]
};
export enum BillingPlan {
STARTER = 'tier-0',
PRO = 'tier-1',
+1
View File
@@ -30,3 +30,4 @@ export { default as InputProjectId } from './inputProjectId.svelte';
export { default as InputDate } from './inputDate.svelte';
export { default as InputDateRange } from './inputDateRange.svelte';
export { default as InputTime } from './inputTime.svelte';
export { default as InputDigits } from './inputDigits.svelte';
@@ -39,6 +39,7 @@
aria-checked={value}
bind:this={element}
bind:checked={value}
on:change
on:invalid={handleInvalid} />
<div class="choice-item-content" class:u-width-full-line={fullWidth}>
+51
View File
@@ -0,0 +1,51 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem } from '.';
import { createPinInput, melt } from '@melt-ui/svelte';
export let length: number = 6;
export let value: string = '';
export let required = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let fullWidth = false;
let element: HTMLOListElement;
const {
elements: { root, input }
} = createPinInput({
placeholder: '',
defaultValue: value.split(''),
onValueChange: ({ next }) => {
value = next.join('');
return next;
}
});
onMount(() => {
if (element && autofocus) {
element.querySelector('input')?.focus();
}
});
</script>
<FormItem {fullWidth}>
<ol class="u-flex u-main-center u-gap-16" use:melt={$root} bind:this={element}>
{#each Array.from({ length }) as _}
<li>
<input
type="number"
class="verification-code-input u-remove-input-number-buttons"
maxlength="1"
style:--p-input-size="3.75rem"
style:font-size="2rem"
use:melt={$input()}
{required}
{readonly}
{disabled} />
</li>
{/each}
</ol>
</FormItem>
+6 -3
View File
@@ -14,9 +14,10 @@
export let autofocus = false;
export let autocomplete = false;
export let maxlength: number = null;
export let isPopoverDefined = true;
// https://www.geeksforgeeks.org/how-to-validate-a-domain-name-using-regular-expression/
const pattern = String.raw`^(?!-)[A-Za-z0-9-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}$`;
const pattern = String.raw`(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}`;
let element: HTMLInputElement;
let error: string;
@@ -50,7 +51,7 @@
<FormItem>
<Label {required} hide={!showLabel} for={id}>
{label}{#if $$slots.popover}
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
@@ -64,7 +65,9 @@
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
+5 -2
View File
@@ -17,6 +17,7 @@
export let autofocus = false;
export let autocomplete = false;
export let tooltip: string = null;
export let isPopoverDefined = true;
let element: HTMLInputElement;
let error: string;
@@ -59,7 +60,7 @@
<FormItem>
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
{label}{#if $$slots.popover}
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
@@ -73,7 +74,9 @@
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
+5 -2
View File
@@ -13,6 +13,7 @@
export let optionalText: string = null;
export let tooltip: string = null;
export let error: string = null;
export let isPopoverDefined = true;
let input: HTMLInputElement;
let hovering = false;
@@ -95,7 +96,7 @@
<div>
{#if label}
<Label {required} {optionalText} {tooltip} hide={!label}>
{label}{#if $$slots.popover}
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
@@ -109,7 +110,9 @@
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
+25 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { Drop } from '$lib/components';
export let id: string;
export let label: string;
@@ -15,10 +16,12 @@
export let showPasswordButton = false;
export let minlength = 8;
export let maxlength: number = null;
export let isPopoverDefined = true;
let element: HTMLInputElement;
let error: string;
let showInPlainText = false;
let showPopover = false;
onMount(() => {
if (element && autofocus) {
@@ -48,7 +51,28 @@
<FormItem>
<Label {required} hide={!showLabel} for={id}>
{label}
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show={showPopover} display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (showPopover = !showPopover)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div class="input-text-wrapper" style={showPasswordButton ? '--amount-of-buttons: 1' : ''}>
+25 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { Drop } from '$lib/components';
export let label: string;
export let showLabel = true;
@@ -13,11 +14,13 @@
export let autofocus = false;
export let autocomplete = false;
export let maxlength: number = null;
export let isPopoverDefined = true;
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
@@ -47,7 +50,28 @@
<FormItem>
<Label {required} hide={!showLabel} for={id}>
{label}
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div class="input-text-wrapper">
<input
@@ -24,7 +24,6 @@
export let hideRequired = false;
export let disabled = false;
export let fullWidth = false;
export let fullWidthDrop = true;
export let autofocus = false;
export let interactiveOutput = false;
// stretch is used inside of a flex container to give the element flex:1
@@ -120,7 +119,6 @@
scrollable
placement="bottom-end"
position="static"
fullWidth={fullWidthDrop}
fixed>
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id} {tooltip}>
{label}
@@ -4,7 +4,6 @@
export let label: string;
export let id: string;
export let value = false;
export let required = false;
export let disabled = false;
let element: HTMLInputElement;
@@ -13,10 +12,6 @@
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
};
@@ -31,7 +26,6 @@
<input
{id}
{disabled}
{required}
type="checkbox"
class="switch"
role="switch"
+2 -1
View File
@@ -12,6 +12,7 @@
export let disabled = false;
export let readonly = false;
export let required = false;
export let tooltip: string = null;
let value = '';
let element: HTMLInputElement;
@@ -77,7 +78,7 @@
value={tags.join(',')}
{required}
on:invalid={handleInvalid} />
<Label {required} hide={!showLabel} for={id}>
<Label {required} {tooltip} hide={!showLabel} for={id}>
{label}
</Label>
+3 -1
View File
@@ -91,7 +91,9 @@
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
+10 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { getServiceLimit, type PlanServices } from '$lib/stores/billing';
import { wizard } from '$lib/stores/wizard';
import { isCloud } from '$lib/system';
@@ -10,6 +11,7 @@
export let service: PlanServices = null;
export let name = service;
export let total: number = null;
export let event: string = null;
let columns = 0;
@@ -34,7 +36,14 @@
<span class="u-flex u-gap-24 u-main-center u-cross-center">
<slot name="limit" {upgradeMethod} {limit}>
<span class="text">Upgrade your plan to add {name} to your organization</span>
<Button secondary on:click={upgradeMethod}>Upgrade plan</Button>
<Button
secondary
on:click={upgradeMethod}
on:click={() =>
trackEvent('click_organization_upgrade', {
from: 'button',
source: event ?? 'table_row_limit_reached'
})}>Upgrade plan</Button>
</slot>
</span>
</td>
+1 -1
View File
@@ -36,7 +36,7 @@ export function getQuery(url: URL): string | undefined {
return url.searchParams.get('query') ?? undefined;
}
type TabElement = { href: string; title: string; hasChildren?: boolean };
export type TabElement = { href: string; title: string; event: string; hasChildren?: boolean };
export function isTabSelected(
tab: TabElement,
+9
View File
@@ -20,3 +20,12 @@ export function formatNumberWithCommas(number: number): string {
const formatter = new Intl.NumberFormat('en');
return formatter.format(number);
}
export function formatCurrency(number: number, locale = 'en-US', currency = 'USD'): string {
if (isNaN(number)) return String(number);
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency
});
return formatter.format(number);
}
+6 -2
View File
@@ -24,13 +24,17 @@ export function bytesToSize(value: number, unit: Size) {
return value / Math.pow(1024, index);
}
export function humanFileSize(bytes: number): {
export function humanFileSize(
bytes: number,
useBits = false
): {
value: string;
unit: Size;
} {
if (typeof bytes !== 'number') return { value: '0', unit: 'Bytes' };
const value = prettyBytes(bytes, {
locale: 'en'
locale: 'en',
bits: useBits
}).split(' ');
return {
+4 -4
View File
@@ -73,12 +73,12 @@ export function createTimeUnitPair(initialValue = 0) {
return { ...createValueUnitPair(initialValue, units), units };
}
export function createByteUnitPair(initialValue = 0) {
export function createByteUnitPair(initialValue = 0, base = 1024) {
const units: Unit[] = [
{ name: 'Bytes', value: 1 },
{ name: 'Kilobytes', value: 1024 },
{ name: 'Megabytes', value: 1024 ** 2 },
{ name: 'Gigabytes', value: 1024 ** 3 }
{ name: 'Kilobytes', value: base },
{ name: 'Megabytes', value: base ** 2 },
{ name: 'Gigabytes', value: base ** 3 }
];
return { ...createValueUnitPair(initialValue, units), units };
}
-77
View File
@@ -1,77 +0,0 @@
import { page } from '$app/stores';
import { get } from 'svelte/store';
import type { Metric } from 'web-vitals';
type Options = {
params:
| {
[s: string]: string;
}
| ArrayLike<string>;
path: string;
analyticsId: string;
debug?: boolean;
};
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
return 'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in (navigator['connection'] as Record<string, unknown>)
? navigator['connection']['effectiveType']
: '';
}
function sendToAnalytics(metric: Metric, options: Options) {
const page = Object.entries(options.params).reduce(
(acc, [key, value]) => acc.replace(value, `[${key}]`),
options.path
);
const body = {
dsn: options.analyticsId,
id: metric.id,
page,
href: location.href,
event_name: metric.name,
value: metric.value.toString(),
speed: getConnectionSpeed()
};
if (options.debug) {
console.debug('[Analytics]', metric.name, JSON.stringify(body, null, 2));
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded'
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true
});
}
export function reportWebVitals(metric: Metric) {
try {
sendToAnalytics(metric, createWebVitalsOptions());
} catch (err) {
console.error('[Analytics]', err);
}
}
function createWebVitalsOptions(): Options {
const ctx = get(page);
return {
path: ctx.url.pathname,
params: ctx.params,
analyticsId: window.VERCEL_ANALYTICS_ID || ''
};
}
+13 -3
View File
@@ -18,6 +18,7 @@
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { ContainerButton } from '.';
import { trackEvent } from '$lib/actions/analytics';
export let isFlex = true;
export let title: string;
@@ -99,8 +100,12 @@
<span class="text">
You've reached the {services} limit for the {tier} plan. <Button
link
on:click={upgradeMethod}>Upgrade</Button> your organization for additional
resources.
on:click={upgradeMethod}
on:click={() =>
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'inline_alert'
})}>Upgrade</Button> your organization for additional resources.
</span>
</Alert>
{/if}
@@ -131,7 +136,12 @@
{title.toLocaleLowerCase()} per project on the {tier} plan.
{#if $organization?.billingPlan === BillingPlan.STARTER}<Button
link
on:click={upgradeMethod}>Upgrade</Button>
on:click={upgradeMethod}
on:click={() =>
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'resource_limit_tag'
})}>Upgrade</Button>
for addtional {title.toLocaleLowerCase()}.
{/if}
</p>
+7 -1
View File
@@ -121,7 +121,13 @@
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
<Button
disabled={$organization?.markedForDeletion}
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
on:click={() => {
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'top_nav'
});
}}>
Upgrade
</Button>
{/if}
+1 -1
View File
@@ -36,7 +36,7 @@
class={icon ? `icon-${icon}` : ''}
aria-hidden="true" />
</div>
<div class="alert-sticky-content">
<div class="alert-sticky-content" data-private>
{#if title}
<h4 class="alert-sticky-title">{title}</h4>
{/if}
+22
View File
@@ -523,6 +523,28 @@ export class Billing {
);
}
async retryPayment(
organizationId: string,
invoiceId: string,
paymentMethodId: string
): Promise<Invoice> {
const path = `/organizations/${organizationId}/invoices/${invoiceId}/payments`;
const params = {
organizationId,
invoiceId,
paymentMethodId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
'post',
uri,
{
'content-type': 'application/json'
},
params
);
}
async listUsage(
organizationId: string,
startDate: string = undefined,
+2 -35
View File
@@ -5,14 +5,13 @@ import { organization, type Organization } from './organization';
import type { InvoiceList, AddressesList, Invoice, PaymentList, PlansMap } from '$lib/sdk/billing';
import { isCloud } from '$lib/system';
import { cachedStore } from '$lib/helpers/cache';
import { Query, type Models } from '@appwrite.io/console';
import { Query } from '@appwrite.io/console';
import { headerAlert } from './headerAlert';
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
import { addNotification, notifications } from './notifications';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import TooManyFreOrgs from '$lib/components/billing/alerts/tooManyFreeOrgs.svelte';
import { activeHeaderAlert, showPostReleaseModal } from '$routes/console/store';
import { activeHeaderAlert } from '$routes/console/store';
import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte';
import { BillingPlan } from '$lib/constants';
@@ -266,35 +265,3 @@ export function checkForMarkedForDeletion(org: Organization) {
});
}
}
export async function checkForFreeOrgOverflow(orgs: Models.TeamList<Record<string, unknown>>) {
if (orgs?.teams?.length > 1) {
headerAlert.add({
id: 'freeOrgOverflow',
component: TooManyFreOrgs,
show: true,
importance: 10
});
activeHeaderAlert.set(headerAlert.get());
}
}
export async function checkForPostReleaseProModal(orgs: Models.TeamList<Record<string, unknown>>) {
if (!orgs?.teams?.length) return;
if (orgs.total > orgs.teams.length) return; // if the total is greater that the free orgs it means that there are pro orgs
const modalTime = localStorage.getItem('postReleaseProModal');
const now = Date.now();
// show the modal if it was never shown
if (!modalTime) {
localStorage.setItem('postReleaseProModal', Date.now().toString());
showPostReleaseModal.set(true);
} else {
const interval = 5 * 24 * 60 * 60 * 1000;
const sinceLastModal = now - parseInt(modalTime);
// show the modal if it was shown more than 5 days ago
if (sinceLastModal > interval) {
localStorage.setItem('postReleaseProModal', Date.now().toString());
showPostReleaseModal.set(true);
}
}
}
+8 -2
View File
@@ -101,9 +101,15 @@ export async function submitStripeCard(name: string, urlRoute?: string) {
}
}
export async function confirmPayment(orgId: string, clientSecret: string, paymentMethodId: string) {
export async function confirmPayment(
orgId: string,
clientSecret: string,
paymentMethodId: string,
route?: string
) {
try {
const url = `${window.location.origin}/console/organization-${orgId}/billing`;
const url =
window.location.origin + (route ? route : `/console/organization-${orgId}/billing`);
const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentMethodId);
+1 -1
View File
@@ -23,4 +23,4 @@ export const MODE = VARS.CONSOLE_MODE === Mode.CLOUD ? Mode.CLOUD : Mode.SELF_HO
export const isCloud = MODE === Mode.CLOUD;
export const isSelfHosted = MODE !== Mode.CLOUD;
export const hasStripePublicKey = !!VARS.STRIPE_PUBLIC_KEY;
export const GRACE_PERIOD_OVERRIDE = true;
export const GRACE_PERIOD_OVERRIDE = false;
-17
View File
@@ -4,7 +4,6 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { isTrackingAllowed, trackPageView } from '$lib/actions/analytics';
import { reportWebVitals } from '$lib/helpers/vitals';
import { Notifications, Progress } from '$lib/layout';
import { app } from '$lib/stores/app';
import { user } from '$lib/stores/user';
@@ -12,32 +11,16 @@
import * as Sentry from '@sentry/svelte';
import LogRocket from 'logrocket';
import { onMount } from 'svelte';
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';
import Loading from './loading.svelte';
import { loading, requestedMigration } from './store';
import { parseIfString } from '$lib/helpers/object';
import Consent, { consent } from '$lib/components/consent.svelte';
if (browser) {
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
}
onMount(async () => {
if ($page.url.searchParams.has('migrate')) {
const migrateData = $page.url.searchParams.get('migrate');
requestedMigration.set(parseIfString(migrateData) as Record<string, string>);
}
/**
* Reporting Web Vitals.
*/
if (ENV.PROD && window.VERCEL_ANALYTICS_ID) {
onCLS(reportWebVitals);
onFID(reportWebVitals);
onLCP(reportWebVitals);
onFCP(reportWebVitals);
onINP(reportWebVitals);
onTTFB(reportWebVitals);
}
if (ENV.PROD) {
/**
+8 -3
View File
@@ -6,12 +6,17 @@ import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import { redirectTo } from './store';
export const ssr = false;
export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
redirectTo.set(url.searchParams.get('forceRedirect') || null);
url.searchParams.delete('forceRedirect');
try {
const account = await sdk.forConsole.account.get<{ organization?: string }>();
@@ -22,7 +27,7 @@ export const load: LayoutLoad = async ({ depends, url }) => {
return {
account,
organizations: sdk.forConsole.teams.list()
organizations: await sdk.forConsole.teams.list()
};
} catch (error) {
const acceptedRoutes = [
@@ -43,11 +48,11 @@ export const load: LayoutLoad = async ({ depends, url }) => {
if (error.type === 'user_more_factors_required') {
if (url.pathname === '/mfa') return;
throw redirect(303, `/mfa${path}`);
redirect(303, `/mfa${path}`);
}
if (!acceptedRoutes.some((n) => url.pathname.startsWith(n))) {
throw redirect(303, `/login${path}`);
redirect(303, `/login${path}`);
}
}
};
+2 -4
View File
@@ -4,9 +4,7 @@ import type { PageLoad } from './$types';
import { user } from '$lib/stores/user';
export const load: PageLoad = async () => {
if (user) {
return;
} else {
throw redirect(302, `${base}/login`);
if (!user) {
redirect(302, `${base}/login`);
}
};
+1 -2
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { Heading } from '$lib/components';
import { Account, Client } from '@appwrite.io/console';
@@ -18,7 +17,7 @@
const secret = $page.url.searchParams.get('secret');
await account.updateMagicURLSession(userId, secret);
await goto(`appwrite-callback-${projectId}://${$page.url.search}`);
window.location.href = `appwrite-callback-${projectId}://${$page.url.search}`;
});
</script>
+2 -7
View File
@@ -1,20 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { Heading } from '$lib/components';
const project = $page.url.searchParams.get('project');
const link = `appwrite-callback-${project}://${$page.url.search}`;
const redirect = new Promise((resolve, reject) => {
const redirect = new Promise((_resolve, reject) => {
if (!project) {
reject('no-project');
}
// this timeout is needed because goto does not
// throw an exception if the redirect does not work
setTimeout(() => reject('timeout'), 500);
// goto will resolve on successful redirect
goto(link).then(resolve);
window.location.href = link;
});
</script>
+2 -7
View File
@@ -1,20 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { Heading } from '$lib/components';
const project = $page.url.searchParams.get('project');
const link = `appwrite-callback-${project}://${$page.url.search}`;
const redirect = new Promise((resolve, reject) => {
const redirect = new Promise((_resolve, reject) => {
if (!project) {
reject('no-project');
}
// this timeout is needed because goto does not
// throw an exception if the redirect does not work
setTimeout(() => reject('timeout'), 500);
// goto will resolve on successful redirect
goto(link).then(resolve);
window.location.href = link;
});
</script>
+3 -3
View File
@@ -2,9 +2,9 @@ import { isCloud } from '$lib/system';
import { redirect } from '@sveltejs/kit';
export async function load({ parent }) {
if (!isCloud) throw redirect(303, '/');
if (!isCloud) redirect(303, '/');
const { account } = await parent();
if (!account) throw redirect(303, '/login');
if (!account) redirect(303, '/login');
throw redirect(303, `/card/${account.$id}`);
redirect(303, `/card/${account.$id}`);
}
+2 -2
View File
@@ -36,7 +36,7 @@ export async function load({ params, url, parent }) {
const res = await fetch(frontImg);
if (!res.ok) {
throw redirect(303, '/');
redirect(303, '/');
}
return {
@@ -53,6 +53,6 @@ export async function load({ params, url, parent }) {
};
} catch (e) {
console.error(e);
throw redirect(303, '/');
redirect(303, '/');
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

@@ -1,242 +0,0 @@
<script lang="ts">
import { Card, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import WizardCover from '$lib/layout/wizardCover.svelte';
import { fade } from 'svelte/transition';
import Hoodie from './hoodie.png';
import { Confetti } from 'svelte-confetti';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { wizard } from '$lib/stores/wizard';
import { organization } from '$lib/stores/organization';
import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg';
import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg';
$: postText = encodeURIComponent(
[
`@appwrite Pro just launched!`,
``,
`I am one of the first to join 🔥`,
``,
`Learn more about Appwrite Pro here: https://apwr.dev/AppwritePro`,
``,
`Ps. Limited edition Appwrite Pro zipper hoodies are up for grabs.`,
``,
`#AppwritePro`
].join('\n')
);
const confettiColors = [
'hsl(var(--color-primary-100))',
'hsl(var(--color-primary-200))',
'hsl(var(--color-primary-300))',
'#F05088',
'#FF86BB',
'#FFFFFF80',
'#FE9567',
'#85DBD8',
'#E5E1FF'
];
$: href = $organization?.$id
? `${base}/console/organization-${$organization.$id}`
: `${base}/console`;
</script>
<WizardCover>
<svelte:fragment slot="header">
<div class="u-flex u-main-space-between">
<a {href}>
<img
src={$app.themeInUse == 'dark' ? AppwriteLogoDark : AppwriteLogoLight}
width="120"
height="22"
alt="Appwrite" />
</a>
<button
on:click={wizard.hide}
class="button is-text is-only-icon"
style:--button-size="1.5rem"
aria-label="close popup">
<span class="icon-x" aria-hidden="true" />
</button>
</div>
</svelte:fragment>
<div class="wizard-container u-flex-vertical">
<div class="u-flex u-cross-center u-main-center hoodie-container">
<Card>
<div class="appwrite-pro">
<span class="text">APPWRITE</span>
<span class="appwrite-pro-text">
<span class="appwrite-pro-text-letter">P</span><span
class="appwrite-pro-text-letter">R</span
><span class="appwrite-pro-text-letter">O</span></span>
</div>
<Heading class="u-margin-block-start-24" tag="h6" size="6">
Your organization has been upgraded 🎉
</Heading>
<p class="text">
Gear up for your new Pro plan! Share online for a chance to win an exclusive
Appwrite Pro hoodie.
</p>
<div class="u-margin-block-start-16 u-flex u-gap-16">
<Button secondary href="https://x.com/intent/tweet?text={postText}" external>
<span class="text">Share on </span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="16"
viewBox="0 0 18 16"
fill="none">
<path
d="M13.8885 0.316772H16.4953L10.8002 6.82583L17.5 15.6832H12.2541L8.14539 10.3113L3.44405 15.6832H0.835697L6.92711 8.72103L0.5 0.316772H5.87904L9.59299 5.22694L13.8885 0.316772ZM12.9736 14.1229H14.418L5.09417 1.7951H3.54413L12.9736 14.1229Z"
fill={$app?.themeInUse === 'dark' ? '#C3C3C6' : '#414146'} />
</svg>
</Button>
</div>
<div class="common-section card-separator u-flex u-main-end">
<Button on:click={wizard.hide}>Go to console</Button>
</div>
</Card>
<img
class="hoodie-image"
src={Hoodie}
alt=""
srcset=""
style="aspect-ratio: 1.56 / 1;" />
<div class="cbc-confetti" transition:fade>
<Confetti
x={[-1.75, 1.85]}
y={[-1.875, 1]}
amount={200}
size={10}
infinite
delay={[2000, 7000]}
colorArray={confettiColors}
fallDistance="200px" />
</div>
</div>
</div>
</WizardCover>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_common.scss';
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
@import '@appwrite.io/pink/src/abstract/functions/_pxToRem.scss';
.wizard-container {
height: 100%;
justify-content: center;
@media #{$break2} {
justify-content: flex-start;
margin-block-start: 2rem;
}
@media #{$break1} {
justify-content: flex-start;
margin-block-start: 2rem;
}
}
.hoodie-container {
height: 350px;
gap: 4rem;
z-index: 1;
@media #{$break2} {
flex-wrap: wrap;
justify-content: start;
align-items: start;
gap: 2rem;
}
@media #{$break1} {
flex-wrap: wrap;
justify-content: start;
align-items: start;
gap: 2rem;
}
}
.hoodie-image {
height: 100%;
object-fit: contain;
@media #{$break2} {
width: 100%;
align-self: start;
height: auto;
}
@media #{$break1} {
align-self: start;
height: auto;
}
}
.appwrite-pro {
position: relative;
z-index: 1;
display: flex;
align-items: baseline;
color: hsl(var(--color-neutral-100));
@media #{$break2open} {
gap: pxToRem(16);
font-size: pxToRem(20);
letter-spacing: pxToRem(8);
line-height: 120%;
}
@media #{$break1} {
gap: pxToRem(8);
font-size: pxToRem(16);
letter-spacing: pxToRem(4);
line-height: 120%;
}
&-text {
padding: pxToRem(9) pxToRem(14);
border: pxToRem(2) solid hsl(343 98% 60% / 0.2);
border-radius: pxToRem(16);
background: rgba(253, 54, 110, 0.1);
box-shadow: 0 -12.173px 20.289px 0px rgba(253, 54, 110, 0.08) inset;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
&-letter {
width: 1rem;
}
@media #{$break1} {
padding: pxToRem(8) pxToRem(12);
border-radius: pxToRem(8);
}
}
}
:global(.theme-dark) .appwrite-pro {
color: hsl(var(--color-neutral-10));
}
.cbc-confetti {
position: absolute;
top: 40%;
right: 25%;
translate: -50% -50%;
z-index: -1;
}
@keyframes shake {
0% {
translate: 0;
}
25% {
translate: 0 -4px;
}
50% {
translate: 0 0px;
}
75% {
translate: 0 0px;
}
100% {
translate: 0 0px;
}
}
</style>
@@ -1,130 +0,0 @@
<script lang="ts">
import { Box, ModalSideCol } from '$lib/components';
import SideLight from './side-light.png';
import SideDark from './side-dark.png';
import { app } from '$lib/stores/app';
import { Button, FormList } from '$lib/elements/forms';
import { trackEvent } from '$lib/actions/analytics';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '../changeOrganizationTierCloud.svelte';
import { user } from '$lib/stores/user';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import type { Organization } from '$lib/stores/organization';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
export let show = false;
let orgs: { teams: Organization[]; total: number };
let selectedOrg: string;
onMount(async () => {
orgs = (await sdk.forConsole.teams.list()) as { teams: Organization[]; total: number };
});
async function handleUpgrade() {
if (selectedOrg) {
await goto(`${base}/console/organization-${selectedOrg}/billing`);
} else {
const freeOrg = orgs.teams.find((o) => o.billingPlan === 'tier-0');
await goto(`${base}/console/organization-${freeOrg.$id}/billing`);
}
show = false;
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
source: event
});
}
$: isNewUser = new Date($user.$createdAt) > new Date(2023, 11, 19);
//user creation date after the 19th of December 2023
$: event = isNewUser ? 'billing_new_user_modal' : 'billing_release_modal';
$: freeOrgs = orgs?.teams?.filter((o) => o.billingPlan === 'tier-0');
$: options = freeOrgs?.map((o) => ({ label: o.name, value: o.$id }));
</script>
<ModalSideCol bind:show title="Claim your Pro credit today" style="max-width: min(53rem,95%)">
<svelte:fragment slot="side">
{#if $app.themeInUse === 'dark'}
<img
src={SideDark}
style="object-fit: cover; object-position: 30% 100%; height: 100%; width: 100%; aspect-ratio: 1 / 1;"
alt="appwrite pro"
aria-hidden="true"
width="376" />
{:else}
<img
src={SideLight}
style="object-fit: cover; object-position: 30% 100%; height: 100%; width: 100%; aspect-ratio: 1 / 1;"
alt="appwrite pro"
aria-hidden="true"
width="376" />
{/if}
</svelte:fragment>
<div class="u-flex u-flex-vertical u-gap-16">
<p class="text">
Upgrade now and use the code <span class="inline-tag"
>{isNewUser ? 'PRO15' : 'THANKYOU30'}</span>
to claim ${isNewUser ? '15' : '30'} in credits for a Pro plan. Credits will expire on 31
January 2024.
</p>
</div>
<Box>
<b class="body-text-1">Pro: $15 per member per billing period</b>
<p class="u-margin-block-start-8">
For pro developers and teams that need to scale their products. <Button
link
href="http://appwrite.io/pricing"
external
on:click={() => {
trackEvent('click_open_pricing', {
source: event
});
}}>Learn more on our pricing page.</Button>
</p>
{#if freeOrgs?.length > 1 && options?.length}
<FormList class="u-margin-block-start-24">
<InputSelect
label="Organization to upgrade"
placeholder="Select organization"
id="orgs"
bind:value={selectedOrg}
{options} />
</FormList>
{:else}
<ul class="list u-flex-vertical u-gap-8 u-margin-block-start-24">
<li class="u-flex u-gap-8 u-cross-center">
<span class="icon-check-circle" aria-hidden="true" />
<span class="text">Unlimited org. members</span>
</li>
<li class="u-flex u-gap-8 u-cross-center">
<span class="icon-check-circle" aria-hidden="true" />
<span class="text">300GB bandwidth</span>
</li>
<li class="u-flex u-gap-8 u-cross-center">
<span class="icon-check-circle" aria-hidden="true" />
<span class="text">200,000 monthly active users</span>
</li>
<li class="u-flex u-gap-8 u-cross-center">
<span class="icon-check-circle" aria-hidden="true" />
<span class="text">150GB free strorage</span>
</li>
<li class="u-flex u-gap-8 u-cross-center">
<span class="icon-check-circle" aria-hidden="true" />
<span class="text">7 days rolling logs</span>
</li>
<li class="u-flex u-gap-8 u-cross-center">
<span class="icon-check-circle" aria-hidden="true" />
<span class="text">Email support</span>
</li>
</ul>
{/if}
<Button fullWidth class="u-margin-block-start-24" on:click={handleUpgrade}>
Start your 14 day free trial
</Button>
</Box>
</ModalSideCol>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 KiB

+2 -22
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { BillingPlan, INTERVAL } from '$lib/constants';
import { INTERVAL } from '$lib/constants';
import { Logs } from '$lib/layout';
import Footer from '$lib/layout/footer.svelte';
import Header from '$lib/layout/header.svelte';
@@ -19,8 +19,6 @@
checkPaymentAuthorizationRequired,
calculateTrialDay,
paymentExpired,
checkForFreeOrgOverflow,
checkForPostReleaseProModal,
checkForMarkedForDeletion
} from '$lib/stores/billing';
import { goto } from '$app/navigation';
@@ -33,7 +31,6 @@
import { project } from './project-[project]/store';
import { feedback } from '$lib/stores/feedback';
import { VARS, hasStripePublicKey, isCloud } from '$lib/system';
import { sdk } from '$lib/stores/sdk';
import { loadStripe } from '@stripe/stripe-js';
import { stripe } from '$lib/stores/stripe';
import MobileSupportModal from './wizard/support/mobileSupportModal.svelte';
@@ -41,10 +38,8 @@
import ExcesLimitModal from './organization-[organization]/excesLimitModal.svelte';
import { showExcess } from './organization-[organization]/store';
import UsageRates from './wizard/cloudOrganization/usageRates.svelte';
import { activeHeaderAlert, consoleVariables, showPostReleaseModal } from './store';
import { Query } from '@appwrite.io/console';
import { activeHeaderAlert, consoleVariables } from './store';
import { headerAlert } from '$lib/stores/headerAlert';
import PostReleaseModal from './(billing-modal)/postReleaseModal.svelte';
function kebabToSentenceCase(str: string) {
return str
@@ -244,17 +239,6 @@
onMount(async () => {
loading.set(false);
if (isCloud) {
if (!$page.url.pathname.includes('/console/onboarding')) {
const orgs = await sdk.forConsole.teams.list([
Query.equal('billingPlan', BillingPlan.STARTER)
]);
checkForPostReleaseProModal(orgs);
checkForFreeOrgOverflow(orgs);
}
}
setInterval(() => {
checkForFeedback(INTERVAL);
}, INTERVAL);
@@ -346,7 +330,3 @@
{#if isCloud && $showUsageRatesModal}
<UsageRates bind:show={$showUsageRatesModal} tier={$organization?.billingPlan} />
{/if}
{#if isCloud && $showPostReleaseModal && !$page.url.pathname.includes('/console/onboarding')}
<!-- {#if true} -->
<PostReleaseModal show={true} />
{/if}
+3 -3
View File
@@ -8,9 +8,9 @@ export const load: PageLoad = async ({ parent, url }) => {
if (organizations.total) {
const teamId = account.prefs.organization ?? organizations.teams[0].$id;
if (!teamId) {
throw redirect(303, `${base}/console/account/organizations${url.search ?? ''}`);
} else throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
redirect(303, `${base}/console/account/organizations${url.search ?? ''}`);
} else redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`);
} else {
throw redirect(303, `${base}/console/onboarding${url.search ?? ''}`);
redirect(303, `${base}/console/onboarding${url.search ?? ''}`);
}
};
+6 -1
View File
@@ -1,9 +1,14 @@
import Header from './header.svelte';
import Breadcrumbs from './breadcrumbs.svelte';
import type { LayoutLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
import { Dependencies } from '$lib/constants';
export const load: LayoutLoad = async ({ depends }) => {
depends(Dependencies.FACTORS);
export const load: LayoutLoad = async () => {
return {
factors: await sdk.forConsole.account.listFactors(),
header: Header,
breadcrumbs: Breadcrumbs
};
+17 -18
View File
@@ -4,6 +4,8 @@
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorProvider } from '@appwrite.io/console';
@@ -11,25 +13,29 @@
export let showDelete = false;
let code: string;
let error: string;
async function deleteProvider() {
try {
await sdk.forConsole.account.deleteAuthenticator(AuthenticatorProvider.Totp, code);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showDelete = false;
addNotification({
type: 'success',
message: 'The authenticator has been removed'
});
trackEvent(Submit.AccountDelete);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AccountDelete);
} catch (e) {
error = e.message;
trackError(e, Submit.AccountDelete);
}
}
$: if (showDelete) {
code = '';
error = '';
}
</script>
<Modal
@@ -38,20 +44,13 @@
onSubmit={deleteProvider}
icon="exclamation"
state="warning"
bind:error
headerDivider={false}>
<p>Are you sure you want to delete this authentication method from your account?</p>
<div>
<label for="code">Enter the 6-digit one-time code generated by the app</label>
<input
required
bind:value={code}
id="code"
class="u-margin-block-start-12"
type="text"
placeholder="Enter code"
minlength="6"
maxlength="6" />
</div>
<FormList>
<InputDigits autofocus required bind:value={code} />
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
+5 -12
View File
@@ -4,7 +4,7 @@
import { Modal, Output, Copy, Alert } from '$lib/components';
import LoadingDots from '$lib/components/loadingDots.svelte';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { Button, FormList, InputDigits } from '$lib/elements/forms';
import { Table, TableBody, TableCell, TableRow } from '$lib/elements/table';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
@@ -25,6 +25,7 @@
try {
await sdk.forConsole.account.verifyAuthenticator(AuthenticatorFactor.Totp, code);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showSetup = false;
showRecoveryCodes = true;
} catch (error) {
@@ -55,18 +56,10 @@
{/await}
</div>
<hr />
<div>
<FormList>
<label for="code">Enter the 6-digit one-time code generated by the app</label>
<input
required
bind:value={code}
id="code"
class="u-margin-block-start-12"
type="text"
placeholder="Enter code"
minlength="6"
maxlength="6" />
</div>
<InputDigits bind:value={code} autofocus />
</FormList>
{/key}
<svelte:fragment slot="footer">
<Button text on:click={() => (showSetup = false)}>Cancel</Button>
+5
View File
@@ -0,0 +1,5 @@
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
export const factors = derived(page, ($page) => $page.data.factors as Models.MfaProviders);
+50 -64
View File
@@ -3,7 +3,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading, Empty } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputChoice } from '$lib/elements/forms';
import { Button, FormList, InputChoice } from '$lib/elements/forms';
import {
Table,
TableBody,
@@ -16,26 +16,17 @@
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { onMount } from 'svelte';
import Mfa from './mfa.svelte';
import DeleteMfa from './deleteMfa.svelte';
import { factors } from './store';
let mfa: boolean = null;
let showSetup: boolean = false;
let showDelete: boolean = false;
onMount(async () => {
mfa ??= $user.mfa;
});
async function updateMfa() {
try {
await sdk.forConsole.account.updateMFA(mfa);
await sdk.forConsole.account.updateMFA(!$user.mfa);
await invalidate(Dependencies.ACCOUNT);
addNotification({
message: 'MFA has been updated',
type: 'success'
});
trackEvent(Submit.AccountUpdateEmail);
} catch (error) {
addNotification({
@@ -47,60 +38,55 @@
}
</script>
<Form onSubmit={updateMfa}>
<CardGrid>
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
<CardGrid>
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputChoice
type="switchbox"
id="mfa"
label="Multi-factor authentication"
bind:value={mfa} />
</FormList>
<p>Enhance the security of your account by adding authentication factors.</p>
{#if mfa}
{#if $user.totp}
<Table noMargin noStyles transparent>
<TableHeader>
<TableCellHead>Authenticator</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
<TableRow>
<TableCellText title="Authenticator">
TOTP (One-time code)
</TableCellText>
<TableCell>
<Button
on:click={() => (showDelete = true)}
round
text
ariaLabel="Delete authenticator">
<span class="icon-trash" aria-hidden="true" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Button disabled text noMargin>
<span class="icon-plus" />
<span class="text">Add authentication factor</span>
</Button>
{:else}
<Empty on:click={() => (showSetup = true)}>
<p class="text">Add authentication factor</p>
</Empty>
{/if}
<svelte:fragment slot="aside">
<FormList>
<InputChoice
on:change={updateMfa}
type="switchbox"
id="mfa"
label="Multi-factor authentication"
value={$user.mfa} />
</FormList>
<p>Enhance the security of your account by adding authentication factors.</p>
{#if $user.mfa}
{#if $factors.totp}
<Table noMargin noStyles transparent>
<TableHeader>
<TableCellHead>Authenticator</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
<TableRow>
<TableCellText title="Authenticator">
TOTP (One-time code)
</TableCellText>
<TableCell>
<Button
on:click={() => (showDelete = true)}
round
text
ariaLabel="Delete authenticator">
<span class="icon-trash" aria-hidden="true" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Button disabled text noMargin>
<span class="icon-plus" />
<span class="text">Add authentication factor</span>
</Button>
{:else}
<Empty on:click={() => (showSetup = true)}>
<p class="text">Add authentication factor</p>
</Empty>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={mfa === $user.mfa} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
{/if}
</svelte:fragment>
</CardGrid>
<Mfa bind:showSetup />
<DeleteMfa bind:showDelete />
@@ -25,7 +25,6 @@
import { tierToPlan } from '$lib/stores/billing';
import { user } from '$lib/stores/user';
import { feedback } from '$lib/stores/feedback';
import HoodieCover from './(billing-modal)/hoodieCover.svelte';
const dispatch = createEventDispatcher();
@@ -88,6 +87,7 @@
org.$id,
$changeOrganizationTier.couponCode
);
trackEvent(Submit.CreditRedeem);
}
//Add budget
@@ -146,7 +146,6 @@
plan: tierToPlan($changeOrganizationTier.billingPlan)?.name
});
wizard.hide();
wizard.showCover(HoodieCover);
} catch (e) {
addNotification({
type: 'error',
@@ -20,7 +20,6 @@
import { wizard } from '$lib/stores/wizard';
import { tierToPlan } from '$lib/stores/billing';
import AddressDetails from './wizard/cloudOrganization/addressDetails.svelte';
import HoodieCover from './(billing-modal)/hoodieCover.svelte';
async function onFinish() {
await invalidate(Dependencies.ORGANIZATION);
@@ -56,6 +55,12 @@
);
}
//Add coupon
if ($createOrganization?.couponCode) {
await sdk.forConsole.billing.addCredit(org.$id, $createOrganization.couponCode);
trackEvent(Submit.CreditRedeem);
}
//Add collaborators
if ($createOrganization?.collaborators?.length) {
$createOrganization.collaborators.forEach(async (collaborator) => {
@@ -91,9 +96,6 @@
});
wizard.hide();
if (org.billingPlan === BillingPlan.PRO) {
wizard.showCover(HoodieCover);
}
} catch (e) {
addNotification({
type: 'error',
@@ -110,7 +112,8 @@
paymentMethodId: null,
collaborators: [],
billingAddressId: null,
taxId: null
taxId: null,
couponCode: null
};
});
+14 -5
View File
@@ -16,18 +16,27 @@
import { ID } from '@appwrite.io/console';
import { onMount } from 'svelte';
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
import { tierToPlan, type Tier } from '$lib/stores/billing';
import { tierToPlan, type Tier, plansInfo } from '$lib/stores/billing';
import { createOrganization } from '../wizard/cloudOrganization/store';
import { formatCurrency } from '$lib/helpers/numbers';
let name: string;
let id: string;
let showCustomId = false;
let plan: Tier;
const options = [
{ value: BillingPlan.STARTER, label: 'Starter - $0/month' },
{ value: BillingPlan.PRO, label: 'Pro - $15/month + add-ons' }
];
const options = isCloud
? [
{
value: BillingPlan.STARTER,
label: `Starter - ${formatCurrency($plansInfo.get(BillingPlan.STARTER).price)}/month`
},
{
value: BillingPlan.PRO,
label: `Pro - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons`
}
]
: [];
onMount(() => {
if (isCloud) {
@@ -45,6 +45,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
const newPrefs = { ...prefs, organization: null };
sdk.forConsole.account.updatePrefs(newPrefs);
localStorage.removeItem('organization');
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -20,6 +20,8 @@
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { BillingPlan } from '$lib/constants';
import RetryPaymentModal from './retryPaymentModal.svelte';
import { selectedInvoice, showRetryModal } from './store';
$: defaultPaymentMethod = $paymentMethods?.paymentMethods?.find(
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
@@ -30,29 +32,40 @@
);
onMount(async () => {
if (
$page.url.searchParams.has('type') &&
$page.url.searchParams.get('type') === 'upgrade'
) {
wizard.start(ChangeOrganizationTierCloud);
}
if ($page.url.searchParams.has('type')) {
if ($page.url.searchParams.get('type') === 'upgrade') {
wizard.start(ChangeOrganizationTierCloud);
}
if (
$page.url.searchParams.has('invoice') &&
$page.url.searchParams.has('type') &&
$page.url.searchParams.get('type') === 'confirmation'
) {
const invoiceId = $page.url.searchParams.get('invoice');
const invoice = await sdk.forConsole.billing.getInvoice(
$page.params.organization,
invoiceId
);
if (
$page.url.searchParams.has('invoice') &&
$page.url.searchParams.get('type') === 'confirmation'
) {
const invoiceId = $page.url.searchParams.get('invoice');
const invoice = await sdk.forConsole.billing.getInvoice(
$page.params.organization,
invoiceId
);
await confirmPayment(
$organization.$id,
invoice.clientSecret,
$organization.paymentMethodId
);
await confirmPayment(
$organization.$id,
invoice.clientSecret,
$organization.paymentMethodId
);
}
if (
$page.url.searchParams.has('invoice') &&
$page.url.searchParams.get('type') === 'retry'
) {
const invoiceId = $page.url.searchParams.get('invoice');
const invoice = await sdk.forConsole.billing.getInvoice(
$page.params.organization,
invoiceId
);
selectedInvoice.set(invoice);
showRetryModal.set(true);
}
}
if ($page.url.searchParams.has('clientSecret')) {
const clientSecret = $page.url.searchParams.get('clientSecret');
@@ -101,3 +114,7 @@
{/if}
<AvailableCredit />
</Container>
{#if $selectedInvoice}
<RetryPaymentModal bind:show={$showRetryModal} bind:invoice={$selectedInvoice} />
{/if}
@@ -1,5 +1,5 @@
<script lang="ts">
import { CardGrid, Empty, Heading, PaginationInline } from '$lib/components';
import { Alert, CardGrid, Empty, Heading, PaginationInline } from '$lib/components';
import {
Table,
TableBody,
@@ -18,6 +18,10 @@
import AddCreditWizard from './addCreditWizard.svelte';
import { Button } from '$lib/elements/forms';
import AddCreditModal from './addCreditModal.svelte';
import { formatCurrency } from '$lib/helpers/numbers';
import { BillingPlan } from '$lib/constants';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { trackEvent } from '$lib/actions/analytics';
let offset = 0;
let creditList: CreditList = {
@@ -65,51 +69,78 @@
}
</script>
<CardGrid>
<CardGrid hideFooter={$organization?.billingPlan !== BillingPlan.STARTER}>
<Heading tag="h2" size="6">Available credit</Heading>
<p class="text">Appwrite credit will automatically be applied to your next invoice.</p>
<svelte:fragment slot="aside">
<div class="u-flex u-cross-center u-main-space-between">
<h4 class="body-text-1 u-bold">
Credit balance <span class="inline-tag">${balance}</span>
</h4>
{#if creditList?.total}
<Button secondary on:click={handleCredits}>
<span class="icon-plus" aria-hidden="true"></span>
<span class="text">Add credits</span>
</Button>
{/if}
</div>
{#if creditList?.total}
<Table noStyles noMargin>
<TableHeader>
<TableCellHead>Date Added</TableCellHead>
<TableCellHead>Expiry Date</TableCellHead>
<TableCellHead>Amount</TableCellHead>
</TableHeader>
<TableBody>
{#each creditList.credits as credit}
<TableRow>
<TableCellText title="date added">
{toLocaleDate(credit.$createdAt)}
</TableCellText>
<TableCellText title="expiry date">
{toLocaleDate(credit.expiration)}
</TableCellText>
<TableCellText title="amount">
${credit.credits}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="u-flex u-main-space-between">
<p class="text">Total results: {creditList?.total}</p>
<PaginationInline {limit} bind:offset sum={creditList?.total} hidePages />
</div>
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Alert type="info">
<svelte:fragment slot="title">Upgrade to Pro to add credits</svelte:fragment>
Upgrade to a Pro plan to add credits to your organization. For more information on what
you can do with a Pro plan,
<a
class="link"
href="https://appwrite.io/pricing"
target="_blank"
rel="noopener noreferrer">view our pricing guide.</a>
</Alert>
{:else}
<Empty target="credits" on:click={handleCredits}>Add credits</Empty>
<div class="u-flex u-cross-center u-main-space-between">
<div class="u-flex u-gap-8 u-cross-center">
<h4 class="body-text-1 u-bold">Credit balance</h4>
<span class="inline-tag">{formatCurrency(balance)}</span>
</div>
{#if creditList?.total}
<Button secondary on:click={handleCredits}>
<span class="icon-plus" aria-hidden="true"></span>
<span class="text">Add credits</span>
</Button>
{/if}
</div>
{#if creditList?.total}
<Table noStyles noMargin>
<TableHeader>
<TableCellHead>Date Added</TableCellHead>
<TableCellHead>Expiry Date</TableCellHead>
<TableCellHead>Amount</TableCellHead>
</TableHeader>
<TableBody>
{#each creditList.credits as credit}
<TableRow>
<TableCellText title="date added">
{toLocaleDate(credit.$createdAt)}
</TableCellText>
<TableCellText title="expiry date">
{toLocaleDate(credit.expiration)}
</TableCellText>
<TableCellText title="amount">
{formatCurrency(credit.credits)}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="u-flex u-main-space-between">
<p class="text">Total results: {creditList?.total}</p>
<PaginationInline {limit} bind:offset sum={creditList?.total} hidePages />
</div>
{:else}
<Empty target="credits" on:click={handleCredits}>Add credits</Empty>
{/if}
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button
secondary
on:click={() => {
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'billing_add_credits'
});
}}>Upgrade to Pro</Button>
{/if}
</svelte:fragment>
</CardGrid>
@@ -91,7 +91,6 @@
label="Percentage (%) of budget cap"
placeholder="Select a percentage"
id="alerts"
fullWidthDrop={false}
{options}
bind:search
bind:value={selectedAlert}
@@ -8,6 +8,8 @@
import { addNotification } from '$lib/stores/notifications';
import { organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { onMount } from 'svelte';
let capActive = false;
@@ -92,7 +94,19 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={$organization?.billingBudget === budget} submit>Update</Button>
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button
secondary
on:click={() => {
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'billing_budget_cap'
});
}}>Upgrade to Pro</Button>
{:else}
<Button disabled={$organization?.billingBudget === budget} submit>Update</Button>
{/if}
</svelte:fragment>
</CardGrid>
</Form>
@@ -3,6 +3,7 @@
import {
CardGrid,
DropList,
DropListItem,
DropListLink,
EmptySearch,
Heading,
@@ -20,11 +21,14 @@
TableScroll
} from '$lib/elements/table';
import { toLocaleDate } from '$lib/helpers/date';
import { formatCurrency } from '$lib/helpers/numbers';
import type { InvoiceList } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { VARS } from '$lib/system';
import { Query } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { trackEvent } from '$lib/actions/analytics';
import { selectedInvoice, showRetryModal } from './store';
let showDropdown = [];
@@ -87,7 +91,9 @@
{status === 'requires_authentication' ? 'failed' : status}
</Pill>
</TableCell>
<TableCellText title="due">${invoice.amount}</TableCellText>
<TableCellText title="due">
{formatCurrency(invoice.amount)}
</TableCellText>
<TableCell showOverflow>
<DropList
bind:show={showDropdown[i]}
@@ -122,6 +128,21 @@
event="download_invoice">
Download PDF
</DropListLink>
{#if status === 'overdue' || status === 'failed'}
<DropListItem
icon="refresh"
on:click={() => {
$selectedInvoice = invoice;
$showRetryModal = true;
showDropdown[i] = !showDropdown[i];
trackEvent(`click_retry_payment`, {
from: 'button',
source: 'billing_invoice_menu'
});
}}>
Retry payment
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
</TableCell>
@@ -12,9 +12,10 @@
import { sdk } from '$lib/stores/sdk';
import type { Invoice } from '$lib/sdk/billing';
import { Query } from '@appwrite.io/console';
import { abbreviateNumber, formatNumberWithCommas } from '$lib/helpers/numbers';
import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { BillingPlan } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
let currentInvoice: Invoice;
const today = new Date();
@@ -62,7 +63,7 @@
<span class="text u-color-text-gray">Total to-date:</span>
{/if}
<span class="body-text-1">
${isTrial ? 0 : currentPlan?.price}
{isTrial ? formatCurrency(0) : formatCurrency(currentPlan?.price)}
</span>
</p>
</div>
@@ -77,7 +78,7 @@
<span> {extraMembers.value}</span>
{extraMembers.name}
</p>
<p class="text">${extraMembers.amount}</p>
<p class="text">{formatCurrency(extraMembers.amount)}</p>
</li>
</ul>
</div>
@@ -97,7 +98,7 @@
</span>
{excess.name}
</p>
<p class="text">${excess.amount}</p>
<p class="text">{formatCurrency(excess.amount)}</p>
</li>
{/if}
{#if ['users', 'executions'].includes(excess.name)}
@@ -107,13 +108,15 @@
>{abbreviateNumber(excess.value)}</span>
{excess.name}
</p>
<p class="text">${excess.amount}</p>
<p class="text">{formatCurrency(excess.amount)}</p>
</li>
{/if}
{/each}
<li class="u-flex u-main-space-between u-margin-block-start-16">
<p class="body-text-1 u-bold">Total to-date:</p>
<p class="body-text-1 u-bold">${currentInvoice?.amount}</p>
<p class="body-text-1 u-bold">
{formatCurrency(currentInvoice?.amount ?? 0)}
</p>
</li>
</ul>
</div>
@@ -136,7 +139,12 @@
</Button>
<Button
disabled={$organization?.markedForDeletion}
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
on:click={() => wizard.start(ChangeOrganizationTierCloud)}
on:click={() =>
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'billing_tab'
})}>
Upgrade
</Button>
</div>
@@ -145,7 +153,12 @@
<Button
text
disabled={$organization?.markedForDeletion}
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
on:click={() => wizard.start(ChangeOrganizationTierCloud)}
on:click={() =>
trackEvent('click_organization_plan_update', {
from: 'button',
source: 'billing_tab'
})}>
Change plan
</Button>
<Button
@@ -1,16 +1,16 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { CreditCardBrandImage, FakeModal } from '$lib/components';
import { Button, FormList, InputRadio, InputText } from '$lib/elements/forms';
import { FakeModal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { organization } from '$lib/stores/organization';
import { Dependencies } from '$lib/constants';
import { initializeStripe, isStripeInitialized, submitStripeCard } from '$lib/stores/stripe';
import { onDestroy, onMount } from 'svelte';
import { submitStripeCard } from '$lib/stores/stripe';
import { onMount } from 'svelte';
import type { PaymentList } from '$lib/sdk/billing';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Pill } from '$lib/elements';
import { PaymentBoxes } from '$lib/components/billing';
export let show = false;
export let isBackup = false;
@@ -18,10 +18,6 @@
let selectedPaymentMethodId: string;
let name: string;
let error: string;
let element: HTMLDivElement;
let loader: HTMLDivElement;
let observer: MutationObserver;
onMount(async () => {
methods = await sdk.forConsole.billing.listPaymentMethods();
@@ -32,27 +28,6 @@
selectedPaymentMethodId = isBackup
? $organization.backupPaymentMethodId
: $organization.paymentMethodId;
observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length > 0) {
for (let node of Array.from(mutation.addedNodes)) {
if (
node instanceof Element &&
node.className.toLowerCase().includes('__privatestripeelement')
) {
loader.style.display = 'none';
}
}
}
}
}
});
});
onDestroy(() => {
observer.disconnect();
});
async function handleSubmit() {
@@ -105,19 +80,8 @@
}
}
$: if (selectedPaymentMethodId === null && !$isStripeInitialized && show) {
initializeStripe();
}
$: filteredMethods = methods?.paymentMethods.filter((method) => !!method?.last4);
$: if (show) {
isStripeInitialized.set(false);
if (element) {
observer.observe(element, { childList: true });
}
}
$: if (!show) {
selectedPaymentMethodId = null;
}
@@ -131,70 +95,16 @@
headerDivider={false}
title="Replace payment method">
<p class="text">Replace the existing payment method for your organization.</p>
<FormList>
<div class:boxes-wrapper={methods?.total}>
{#if methods?.total}
{#each filteredMethods as method}
<div class="box">
<InputRadio
id={`payment-method-${method.$id}`}
disabled={isBackup
? method.$id === $organization.paymentMethodId
: method.$id === $organization.backupPaymentMethodId}
value={method.$id}
name="payment"
bind:group={selectedPaymentMethodId}>
<span class="u-flex u-gap-16 u-main-space-between">
<span
class="u-flex u-cross-center u-gap-8"
style="padding-inline:0.25rem">
<span>
<span class="u-capitalize">{method.brand}</span> ending in {method.last4}</span>
<CreditCardBrandImage brand={method.brand} />
</span>
{#if method.$id === $organization.backupPaymentMethodId}
<Pill>Backup</Pill>
{:else if method.$id === $organization.paymentMethodId}
<Pill>Default</Pill>
{/if}
</span>
</InputRadio>
</div>
{/each}
{/if}
<div class="box">
{#if methods?.total}
<InputRadio
id="payment-method"
value={null}
name="payment"
bind:group={selectedPaymentMethodId}>
<span style="padding-inline:0.25rem">Add new payment method</span>
</InputRadio>
{/if}
{#if selectedPaymentMethodId === null}
<FormList>
<InputText
id="name"
label="Cardholder name"
placeholder="Cardholder name"
bind:value={name}
required
autofocus={true} />
<div class="aw-stripe-container" data-private>
<div class="loader-container" bind:this={loader}>
<div class="loader"></div>
</div>
<div id="payment-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
</div>
</FormList>
{/if}
</div>
</div>
</FormList>
<PaymentBoxes
methods={filteredMethods}
bind:name
bind:group={selectedPaymentMethodId}
defaultMethod={$organization?.paymentMethodId}
backupMethod={$organization?.backupPaymentMethodId}
disabledCondition={isBackup
? $organization.paymentMethodId
: $organization.backupPaymentMethodId} />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button
@@ -206,17 +116,3 @@
</Button>
</svelte:fragment>
</FakeModal>
<style lang="scss">
.aw-stripe-container {
min-height: 295px;
position: relative;
.loader-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
}
}
</style>
@@ -0,0 +1,135 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { FakeModal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Dependencies } from '$lib/constants';
import type { Invoice } from '$lib/sdk/billing';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { page } from '$app/stores';
import { VARS } from '$lib/system';
import {
confirmPayment,
initializeStripe,
isStripeInitialized,
submitStripeCard
} from '$lib/stores/stripe';
import { organization } from '$lib/stores/organization';
import { toLocaleDate } from '$lib/helpers/date';
import { PaymentBoxes } from '$lib/components/billing';
import { paymentMethods } from '$lib/stores/billing';
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${$page.url.origin}/v1`;
export let show = false;
export let invoice: Invoice;
let error: string = null;
let isButtonDisabled = false;
let name: string;
let paymentMethodId: string;
let setAsDefault = false;
onMount(() => {
paymentMethodId = $organization.paymentMethodId;
});
async function handleSubmit() {
isButtonDisabled = true;
try {
if (paymentMethodId === null) {
try {
const method = await submitStripeCard(name);
const card = await sdk.forConsole.billing.getPaymentMethod(method.$id);
if (card?.last4) {
paymentMethodId = card.$id;
} else {
throw new Error(
'The payment method you selected is not valid. Please select a different one.'
);
}
invalidate(Dependencies.PAYMENT_METHODS);
} catch (e) {
paymentMethodId = $organization.paymentMethodId;
error = e.message;
}
}
if (setAsDefault) {
await sdk.forConsole.billing.setDefaultPaymentMethod(paymentMethodId);
}
const { clientSecret } = await sdk.forConsole.billing.retryPayment(
$organization.$id,
invoice.$id,
paymentMethodId
);
await confirmPayment(
$organization.$id,
clientSecret,
paymentMethodId ? paymentMethodId : $organization.paymentMethodId
);
invalidate(Dependencies.ORGANIZATION);
invalidate(Dependencies.INVOICES);
trackEvent(Submit.RetryPayment);
addNotification({
type: 'success',
message: `Payment has been successfully processed`
});
show = false;
} catch (e) {
error = e.message;
trackError(e, Submit.RetryPayment);
} finally {
isButtonDisabled = false;
}
}
$: if (paymentMethodId === null && !$isStripeInitialized) {
initializeStripe();
}
$: if (paymentMethodId) {
isStripeInitialized.set(false);
}
$: filteredMethods = $paymentMethods?.paymentMethods.filter((method) => !!method?.last4);
$: if (!show) {
invoice = null;
}
</script>
<FakeModal
bind:show
bind:error
state="warning"
onSubmit={handleSubmit}
size="big"
headerDivider={false}
title="Retry payment">
<!-- TODO: format currency -->
<p class="text">
Your payment of <span class="inline-tag">${invoice.amount}</span> due on {toLocaleDate(
invoice.dueAt
)} has failed. Retry your payment to avoid service interruptions with your projects.
</p>
<Button
external
link
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/view`}>
View invoice
</Button>
<PaymentBoxes
methods={filteredMethods}
defaultMethod={$organization?.paymentMethodId}
backupMethod={$organization?.backupPaymentMethodId}
bind:setAsDefault
bind:name
bind:group={paymentMethodId} />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit disabled={isButtonDisabled}>Retry</Button>
</svelte:fragment>
</FakeModal>
@@ -1,6 +1,6 @@
import { page } from '$app/stores';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import type { AggregationList } from '$lib/sdk/billing';
import type { AggregationList, Invoice } from '$lib/sdk/billing';
import { derived, writable } from 'svelte/store';
export const aggregationList = derived(
@@ -13,3 +13,6 @@ export const addCreditWizardStore = writable<{ coupon: string; paymentMethodId:
coupon: null,
paymentMethodId: null
});
export const selectedInvoice = writable<Invoice>(null);
export const showRetryModal = writable(false);
@@ -4,6 +4,7 @@
import type { Coupon } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { onMount } from 'svelte';
import { addCreditWizardStore } from '../store';
let coupon: string;
@@ -18,6 +19,7 @@
try {
const response = await sdk.forConsole.billing.getCoupon(coupon);
couponData = response;
$addCreditWizardStore.coupon = coupon;
coupon = null;
} catch (error) {
couponData.code = coupon;
@@ -26,6 +28,13 @@
throw new Error(error);
}
}
onMount(() => {
if ($addCreditWizardStore.coupon) {
coupon = $addCreditWizardStore.coupon;
validateCoupon();
}
});
</script>
<WizardStep beforeSubmit={validateCoupon}>
@@ -41,6 +50,6 @@
$addCreditWizardStore.coupon = e.detail.code;
}}
let:data>
<span>{data.code} has been successfully added</span>
<span>{data.code.toUpperCase()} has been successfully added</span>
</CouponInput>
</WizardStep>
@@ -11,6 +11,7 @@
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { isCloud } from '$lib/system';
import { plansInfo } from '$lib/stores/billing';
import { formatCurrency } from '$lib/helpers/numbers';
export let showCreate = false;
@@ -63,7 +64,7 @@
You can add unlimited organization members on the {plan.name} plan at no cost.
{:else if $organization?.billingPlan === BillingPlan.PRO}
You can add unlimited organization members on the {plan.name} plan for
<b>${plan.addons.member.price} each per billing period</b>.
<b>{formatCurrency(plan.addons.member.price)} each per billing period</b>.
{/if}
</Alert>
{/if}
@@ -14,6 +14,7 @@
import { goto } from '$app/navigation';
import { last } from '$lib/helpers/array';
import { BillingPlan } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
export let show = false;
const plan = $plansInfo?.get($organization.billingPlan);
@@ -62,6 +63,7 @@
has reached the limits of the {tierToPlan($organization.billingPlan).name} plan. Excess usage fees will apply.
</p>
<PlanExcess {excess} currentTier={$organization.billingPlan} />
<svelte:fragment slot="footer">
@@ -73,13 +75,20 @@
on:click={() => {
show = false;
goto(`/console/organization-${$organization.$id}/usage`);
}}>View usage</Button>
}}>
View usage
</Button>
<Button
on:click={() => {
show = false;
wizard.start(ChangeOrganizationTierCloud);
}}>Upgrade plan</Button>
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'limit_reached_modal'
});
}}>
Upgrade plan
</Button>
</div>
</div>
</svelte:fragment>
@@ -20,7 +20,6 @@
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import Delete from '../deleteMember.svelte';
import { toLocaleDate } from '$lib/helpers/date';
export let data: PageData;
@@ -64,13 +63,16 @@
<TableScroll>
<TableHeader>
<TableCellHead width={140}>Name</TableCellHead>
<TableCellHead width={160}>Name</TableCellHead>
<TableCellHead width={120}>Email</TableCellHead>
<TableCellHead width={100}>Joined</TableCellHead>
<TableCellHead width={90} />
<TableCellHead width={90}>2FA</TableCellHead>
<TableCellHead width={60} />
<TableCellHead width={30} />
</TableHeader>
<TableBody service="members" total={data.organizationMembers.total}>
<TableBody
service="members"
total={data.organizationMembers.total}
event="members_list">
{#each data.organizationMembers.memberships as member}
<TableRow>
<TableCell title="Name">
@@ -85,8 +87,17 @@
</div>
</TableCell>
<TableCellText title="Email">{member.userEmail}</TableCellText>
<TableCellText title="Joined">
{member.joined ? toLocaleDate(member.joined) : ''}
<TableCellText title="2FA">
<Pill success={member.mfa}>
{#if member.mfa}
<span
class="icon-check-circle u-color-text-success"
aria-hidden="true" />
<p class="text">enabled</p>
{:else}
<p class="text">disabled</p>
{/if}
</Pill>
</TableCellText>
<TableCell>
{#if member.invited && !member.joined}
@@ -13,6 +13,7 @@
import { accumulateFromEndingTotal, total } from '$lib/layout/usage.svelte';
import type { OrganizationUsage } from '$lib/sdk/billing';
import { BillingPlan } from '$lib/constants';
import { trackEvent } from '$lib/actions/analytics';
export let data;
@@ -42,7 +43,14 @@
<Heading tag="h2" size="5">Usage</Heading>
{#if $organization?.billingPlan === BillingPlan.STARTER}
<Button on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
<Button
on:click={() => {
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'organization_usage'
});
}}>
<span class="text">Upgrade</span>
</Button>
{/if}
@@ -25,6 +25,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
organization: await sdk.forConsole.teams.get(project.teamId)
};
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -3,5 +3,5 @@ import { base } from '$app/paths';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
throw redirect(302, `${base}/console/project-${params.project}/overview`);
redirect(302, `${base}/console/project-${params.project}/overview`);
};
@@ -14,6 +14,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
team: await sdk.forProject.teams.get(params.team)
};
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -11,6 +11,6 @@ export const load: PageLoad = async ({ params }) => {
try {
return sdk.forProject.users.getUsage(period);
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -12,9 +12,10 @@ export const load: LayoutLoad = async ({ params, depends }) => {
return {
header: Header,
breadcrumbs: Breadcrumbs,
user: await sdk.forProject.users.get(params.user)
user: await sdk.forProject.users.get(params.user),
userFactors: await sdk.forProject.users.listProviders(params.user)
};
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -3,6 +3,7 @@
import DangerZone from './dangerZone.svelte';
import UpdateEmail from './updateEmail.svelte';
import UpdateLabels from './updateLabels.svelte';
import UpdateMfa from './updateMfa.svelte';
import UpdateName from './updateName.svelte';
import UpdatePassword from './updatePassword.svelte';
import UpdatePhone from './updatePhone.svelte';
@@ -18,5 +19,6 @@
<UpdatePassword />
<UpdateLabels />
<UpdatePrefs />
<UpdateMfa />
<DangerZone />
</Container>
@@ -0,0 +1,63 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorProvider } from '@appwrite.io/console';
import { user } from './store';
export let showDelete = false;
let code: string;
let error: string;
async function deleteProvider() {
try {
await sdk.forConsole.users.deleteAuthenticator(
$user.$id,
AuthenticatorProvider.Totp,
''
);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showDelete = false;
addNotification({
type: 'success',
message: 'The authenticator has been removed'
});
trackEvent(Submit.AccountDelete);
} catch (e) {
error = e.message;
trackError(e, Submit.AccountDelete);
}
}
$: if (showDelete) {
code = '';
error = '';
}
</script>
<Modal
title="Delete authentication provider"
bind:show={showDelete}
onSubmit={deleteProvider}
icon="exclamation"
state="warning"
bind:error
headerDivider={false}>
<p>Are you sure you want to delete this authentication method from your account?</p>
<FormList>
<InputDigits autofocus required bind:value={code} />
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
@@ -6,3 +6,4 @@ export const user = derived(
page,
($page) => $page.data.user as Models.User<Record<string, string>>
);
export const userFactors = derived(page, ($page) => $page.data.userFactors as Models.MfaProviders);
@@ -0,0 +1,51 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import EmptySearch from '$lib/components/emptySearch.svelte';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCell,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { userFactors } from './store';
</script>
<CardGrid>
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
<p class="text">MFA allow users to enhance the security of their account in you app.</p>
<svelte:fragment slot="aside">
{#if $userFactors.totp}
<Table noMargin noStyles transparent>
<TableHeader>
<TableCellHead>Authenticator</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
<TableRow>
<TableCellText title="Authenticator">TOTP (One-time code)</TableCellText>
<TableCell>
<Button round text ariaLabel="Delete authenticator">
<span class="icon-trash" aria-hidden="true" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Button disabled text noMargin>
<span class="icon-plus" />
<span class="text">Add authentication factor</span>
</Button>
{:else}
<EmptySearch hidePagination hidePages>
<p class="text u-text-center">
No authenticators have been enabled. Once the user adds an authenticator, youll
see it here.
</p>
</EmptySearch>
{/if}
</svelte:fragment>
</CardGrid>
@@ -15,6 +15,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
database: await sdk.forProject.databases.get(params.database)
};
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -23,6 +23,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
])
};
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -86,7 +86,10 @@
label="Elements"
bind:tags={data.elements}
placeholder="Add elements here"
required />
tooltip="Enum elements have a maxiumum length of 255 characters. This limit can not be exceeded."
required>
</InputTags>
<InputSelect
id="default"
label="Default value"
@@ -46,6 +46,6 @@ export const load: LayoutLoad = async ({ params, parent, depends }) => {
document
};
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -11,6 +11,6 @@ export const load: PageLoad = async ({ params }) => {
try {
return sdk.forProject.databases.getUsage(period);
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -8,10 +8,15 @@ import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ depends }) => {
depends(Dependencies.FUNCTION_INSTALLATIONS);
const [runtimesList, installations] = await Promise.all([
sdk.forProject.functions.listRuntimes(),
sdk.forProject.vcs.listInstallations([Query.limit(100)])
]);
return {
header: Header,
breadcrumbs: Breadcrumbs,
runtimesList: sdk.forProject.functions.listRuntimes(),
installations: sdk.forProject.vcs.listInstallations([Query.limit(100)])
runtimesList,
installations
};
};
@@ -22,6 +22,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
])
};
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -31,8 +31,6 @@ export const load: PageLoad = async ({ params, depends }) => {
return {
variables,
globalVariables,
runtimesList: sdk.forProject.functions.listRuntimes(),
installations: sdk.forProject.vcs.listInstallations()
globalVariables
};
};
@@ -11,6 +11,6 @@ export const load: PageLoad = async ({ params }) => {
: FunctionUsageRange.ThirtyDays;
return sdk.forProject.functions.getFunctionUsage(params.function, period);
} catch (e) {
throw error(e.code, e.message);
error(e.code, e.message);
}
};
@@ -169,13 +169,18 @@
</TableCellText>
{:else if column.id === 'status'}
<TableCellText onlyDesktop title="Status">
<MessageStatusPill
status={message.status}
on:click={(e) => {
e.preventDefault();
errors = message.deliveryErrors;
showFailed = true;
}} />
<span class="u-inline-flex u-gap-12 u-cross-center">
<MessageStatusPill status={message.status} />
{#if message.status === 'failed'}
<Button
link
on:click={(e) => {
e.preventDefault();
errors = message.deliveryErrors;
showFailed = true;
}}>Details</Button>
{/if}
</span>
</TableCellText>
{:else if column.type === 'datetime'}
<TableCellText title={column.title} width={column.width}>
@@ -225,19 +230,14 @@
total={data.messages.total} />
{:else if $hasPageQueries}
<EmptyFilter resource="messages" />
<!-- TODO: remove data.search != 'empty' when the API is ready with data -->
{:else if data.search && data.search != 'empty'}
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no messages that match your search.</p>
</div>
<div class="u-flex u-gap-16">
<!-- TODO: update docs link -->
<Button
external
href="https://appwrite.io/docs/products/storage/upload-download"
text>
<Button external href="https://appwrite.io/docs/products/messaging/messages" text>
Documentation
</Button>
<Button secondary href={`/console/project-${$page.params.project}/messaging`}>
@@ -246,12 +246,7 @@
</div>
</EmptySearch>
{:else}
<!-- TODO: update docs link -->
<Empty
single
href="https://appwrite.io/docs"
target="message"
on:click={() => ($showCreate = true)}>
<Empty single target="message" on:click={() => ($showCreate = true)}>
<div class="u-text-center">
<Heading size="7" tag="h2" trimmed={false}>
Create your first message to get started.
@@ -263,7 +258,7 @@
<div class="u-flex u-flex-wrap u-gap-16 u-main-center">
<Button
external
href="https://appwrite.io/docs/references/cloud/client-web/messages"
href="https://appwrite.io/docs/products/messaging/messages"
text
event="empty_documentation"
ariaLabel={`create message`}>
@@ -33,7 +33,7 @@ export const load: PageLoad = async ({ depends, url, route }) => {
query,
page,
view,
messages: sdk.forProject.messaging.listMessages(
messages: (await sdk.forProject.messaging.listMessages(
[
Query.limit(limit),
Query.offset(offset),
@@ -41,9 +41,9 @@ export const load: PageLoad = async ({ depends, url, route }) => {
...parsedQueries.values()
],
search || undefined
) as Promise<{
)) as {
total: number;
messages: (Models.Message & { data: Record<string, string> })[]; // Add typing for message.data
}>
}
};
};

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