mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch '1.5.x' into fix-update-exceed-modal-copy
This commit is contained in:
+1
-1
@@ -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=
|
||||
@@ -0,0 +1,4 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
Generated
+1129
-897
File diff suppressed because it is too large
Load Diff
+14
-15
@@ -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"
|
||||
}
|
||||
|
||||
Vendored
+1
-3
@@ -1,4 +1,2 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
interface Window {
|
||||
VERCEL_ANALYTICS_ID: string | false;
|
||||
}
|
||||
interface Window {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 -->
|
||||
<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>
|
||||
|
||||
@@ -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 -->
|
||||
<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>
|
||||
|
||||
@@ -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 -->
|
||||
<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>
|
||||
|
||||
@@ -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 -->
|
||||
<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' : ''}>
|
||||
|
||||
@@ -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 -->
|
||||
<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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 || ''
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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,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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,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}`);
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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}
|
||||
|
||||
@@ -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 ?? ''}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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, you’ll
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -23,6 +23,6 @@ export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
])
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(e.code, e.message);
|
||||
error(e.code, e.message);
|
||||
}
|
||||
};
|
||||
|
||||
+4
-1
@@ -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"
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
+1
-3
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user