Merge pull request #1052 from appwrite/sync-main-cloud

sync: main <- cloud
This commit is contained in:
Torsten Dittmann
2024-04-26 12:21:14 +02:00
committed by GitHub
39 changed files with 1658 additions and 736 deletions
+569 -492
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -51,8 +51,8 @@
"@testing-library/user-event": "^14.4.3",
"@types/deep-equal": "^1.0.1",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"@vitest/ui": "^1.2.1",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
+3 -2
View File
@@ -2,10 +2,11 @@ import { AppwriteException } from '@appwrite.io/console';
import type { HandleClientError } from '@sveltejs/kit';
export const handleError: HandleClientError = async ({ error, message, status }) => {
if (error instanceof AppwriteException && error.code === 0) {
status = undefined;
if (error instanceof AppwriteException) {
status = error.code === 0 ? undefined : error.code;
message = error.message;
}
return {
message,
status
@@ -0,0 +1,43 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { readOnly, tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { wizard } from '$lib/stores/wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
</script>
{#if $organization?.$id && $organization?.billingPlan === BillingPlan.STARTER && $readOnly && !$page.url.pathname.includes('/console/account')}
<HeaderAlert
type="error"
title={`${$organization.name} usage has reached the ${tierToPlan($organization.billingPlan).name} plan limit`}>
<svelte:fragment>
Usage for the <b>{$organization.name}</b> organization has reached the limits of the Starter
plan. Consider upgrading to increase your resource usage.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
href={`${base}/console/organization-${$organization?.$id}/usage`}
text
fullWidthMobile>
<span class="text">View usage</span>
</Button>
<Button
on:click={() => {
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'limit_reached_banner'
});
}}
secondary
fullWidthMobile>
<span class="text">Upgrade plan</span>
</Button>
</svelte:fragment>
</HeaderAlert>
{/if}
+5
View File
@@ -33,4 +33,9 @@
<svelte:fragment slot="title">This payment method has expired</svelte:fragment>
</Alert>
{/if}
{#if paymentMethod?.lastError}
<Alert type="error" class="u-margin-block-start-16 u-width-full-line">
{paymentMethod.lastError}
</Alert>
{/if}
</div>
-14
View File
@@ -24,9 +24,6 @@
import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte';
import { Feedback } from '$lib/components/feedback';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { Pill } from '$lib/elements';
import { showExcess } from '$routes/console/organization-[organization]/store';
import { readOnly } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
let showDropdown = false;
@@ -105,17 +102,6 @@
<svelte:component this={$page.data.breadcrumbs} />
{/if}
{#if !$page.url.pathname.includes('/console/account') && $readOnly}
<div style="min-inline-size: fit-content">
<Pill danger button on:click={() => ($showExcess = true)}>
<div>
<span class="icon-exclamation-circle" aria-hidden="true" />
<span>limit reached</span>
</div>
</Pill>
</div>
{/if}
<div class="main-header-end">
<nav class="u-flex is-only-desktop u-cross-center">
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
+12 -4
View File
@@ -28,6 +28,7 @@
export let steps: WizardStepsType;
export let confirmExit = false;
export let finalAction = 'Create';
export let finalMethod: () => Promise<void> = null;
const dispatch = createEventDispatcher();
@@ -98,11 +99,18 @@
}
} else {
$wizard.nextDisabled = true;
trackEvent('wizard_finish');
dispatch('finish');
setTimeout(() => {
if (finalMethod) {
await finalMethod();
trackEvent('wizard_finish');
$wizard.nextDisabled = false;
}, 2000);
} else {
trackEvent('wizard_finish');
dispatch('finish');
setTimeout(() => {
$wizard.nextDisabled = false;
}, 2000);
}
}
} else {
if (steps.get($wizard.step + 1)?.disabled) {
+46 -3
View File
@@ -19,6 +19,7 @@ export type PaymentMethodData = {
failed: boolean;
name: string;
mandateId?: string;
lastError?: string;
};
export type PaymentList = {
@@ -48,6 +49,7 @@ export type Invoice = {
rate: number;
desc: string;
}[];
lastError?: string;
};
export type InvoiceList = {
@@ -92,11 +94,11 @@ export type Credit = {
/**
* Provided credit amount
*/
credits: number;
total: number;
/**
* Remaining up credit amount
* Remaining credit amount
*/
creditsRemaining: number;
credits: number;
/**
* Credit expiration time in ISO 8601 format.
*/
@@ -259,6 +261,7 @@ export type Plan = {
storage: AdditionalResource;
users: AdditionalResource;
};
trialDays: number;
};
export type PlansInfo = {
@@ -743,6 +746,46 @@ export class Billing {
);
}
async getOrganizationPaymentMethod(
organizationId: string,
paymentMethodId: string
): Promise<PaymentMethodData> {
const path = `/organizations/${organizationId}/payment-methods/${paymentMethodId}`;
const params = {
organizationId,
paymentMethodId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
'GET',
uri,
{
'content-type': 'application/json'
},
params
);
}
async getOrganizationBillingAddress(
organizationId: string,
billingAddressId: string
): Promise<Address> {
const path = `/organizations/${organizationId}/billing-addresses/${billingAddressId}`;
const params = {
organizationId,
billingAddressId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
'GET',
uri,
{
'content-type': 'application/json'
},
params
);
}
//ACCOUNT
async listPaymentMethods(queries: [] = []): Promise<PaymentList> {
+69 -14
View File
@@ -23,6 +23,10 @@ import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.
import { BillingPlan } from '$lib/constants';
import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte';
import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte';
import LimitReached from '$lib/components/billing/alerts/limitReached.svelte';
import { wizard } from './wizard';
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
import { trackEvent } from '$lib/actions/analytics';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2';
@@ -77,18 +81,18 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null): num
}
export const failedInvoice = cachedStore<
false | Invoice,
Invoice,
{
load: (orgId: string) => Promise<void>;
}
>('failedInvoice', function ({ set }) {
return {
load: async (orgId) => {
if (!isCloud) set(false);
if (!isCloud) set(null);
const invoices = await sdk.forConsole.billing.listInvoices(orgId);
const failedInvoices = invoices.invoices.filter((i) => i.status === 'failed');
// const failedInvoices = invoices.invoices;
if (failedInvoices.length > 0) {
if (failedInvoices?.length > 0) {
const firstFailed = failedInvoices[0];
const today = new Date();
const thirtyDaysAgo = new Date(today.setDate(today.getDate() - 30));
@@ -96,7 +100,7 @@ export const failedInvoice = cachedStore<
if (failedDate < thirtyDaysAgo) {
readOnly.set(true);
}
} else set(false);
} else set(null);
}
};
});
@@ -179,26 +183,74 @@ export function calculateTrialDay(org: Organization) {
}
export async function checkForUsageLimit(org: Organization) {
if (org?.billingPlan !== BillingPlan.STARTER) {
readOnly.set(false);
return;
}
if (!org?.billingLimits) {
readOnly.set(false);
return;
}
const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {};
const resources = [
{ value: bandwidth, name: 'bandwidth' },
{ value: documents, name: 'documents' },
{ value: executions, name: 'executions' },
{ value: storage, name: 'storage' },
{ value: users, name: 'users' }
];
const members = await sdk.forConsole.teams.listMemberships(org.$id);
const plan = get(plansInfo)?.get(org.billingPlan);
const membersOverflow =
members?.total > plan.members ? members.total - (plan.members || members.total) : 0;
if (
bandwidth >= 100 ||
documents >= 100 ||
executions >= 100 ||
storage >= 100 ||
users >= 100 ||
membersOverflow > 0
) {
if (resources.some((r) => r.value >= 100) || membersOverflow > 0) {
readOnly.set(true);
} else readOnly.set(false);
headerAlert.add({
id: 'limitReached',
component: LimitReached,
show: true,
importance: 7
});
} else if (resources.some((r) => r.value >= 75)) {
readOnly.set(false);
const lastNotification = parseInt(localStorage.getItem('limitReachedNotification')) ?? 0;
const now = new Date().getTime();
if (now - lastNotification < 1000 * 60 * 60 * 24) return;
localStorage.setItem('limitReachedNotification', now.toString());
let message = `<b>${org.name}</b> has reached <b>75%</b> of the Starter plan's ${resources.find((r) => r.value >= 75).name} limit. Upgrade to ensure there are no service disruptions.`;
if (resources.filter((r) => r.value >= 75)?.length > 1) {
message = `Usage for <b>${org.name}</b> has reached 75% of the Starter plan limit. Upgrade to ensure there are no service disruptions.`;
}
addNotification({
type: 'warning',
isHtml: true,
timeout: 0,
message,
buttons: [
{
name: 'View usage',
method: () => {
goto(`${base}/console/organization-${org.$id}/usage`);
}
},
{
name: 'Upgrade plan',
method: () => {
wizard.start(ChangeOrganizationTierCloud);
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'limit_reached_notification'
});
}
}
]
});
} else {
readOnly.set(false);
}
}
export async function checkPaymentAuthorizationRequired(org: Organization) {
@@ -223,7 +275,10 @@ export async function checkPaymentAuthorizationRequired(org: Organization) {
export async function paymentExpired(org: Organization) {
if (!org?.paymentMethodId) return;
const payment = await sdk.forConsole.billing.getPaymentMethod(org.paymentMethodId);
const payment = await sdk.forConsole.billing.getOrganizationPaymentMethod(
org.$id,
org.paymentMethodId
);
if (!payment?.expiryYear) return;
const year = new Date().getFullYear();
const month = new Date().getMonth();
+692
View File
@@ -956,6 +956,698 @@ export const marketplace: MarketplaceTemplate[] = [
type: 'text'
}
]
},
{
icon: 'icon-chip',
id: 'text-generation-with-huggingface',
name: 'Text generation',
tagline: 'Generate text using the Hugging Face inference API.',
permissions: ['any'],
events: [],
cron: '',
timeout: 30,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/text-generation-with-huggingface'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/text-generation-with-huggingface">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'HUGGINGFACE_ACCESS_TOKEN',
description: `Secret for sending requests to the Hugging Face API. <a class="u-bold" target="_blank" href="https://huggingface.co/docs/api-inference/en/quicktour#get-your-api-token">Learn more</a>.`,
placeholder: 'hf_MUvn...',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'language-translation-with-huggingface',
name: 'Language translation',
tagline: 'Translate text using the Hugging Face inference API.',
permissions: ['any'],
events: [],
cron: '',
timeout: 30,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/language-translation-with-huggingface'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/language-translation-with-huggingface">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'HUGGINGFACE_ACCESS_TOKEN',
description: `Secret for sending requests to the Hugging Face API. <a class="u-bold" target="_blank" href="https://huggingface.co/docs/api-inference/en/quicktour#get-your-api-token">Learn more</a>.`,
placeholder: 'hf_MUvn...',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'image-classification-with-huggingface',
name: 'Image classification',
tagline: 'Classify images using the Hugging Face inference API.',
permissions: ['any'],
events: ['buckets.*.files.*.create'],
cron: '',
timeout: 15,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install && npm run setup',
'src/main.js',
'node/image-classification-with-huggingface'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/image-classification-with-huggingface">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to authenticate against Appwrite's Server APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/advanced/platform/api-keys">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/references#api">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false,
type: 'url'
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/databases">Learn more</a>.`,
value: 'ai',
placeholder: 'ai',
required: false,
type: 'text'
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/collections">Learn more</a>.`,
value: 'image_classification',
placeholder: 'image_classification',
required: false,
type: 'text'
},
{
name: 'APPWRITE_BUCKET_ID',
description: `The ID of the bucket where the images are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/storage/buckets">Learn more</a>.`,
value: 'image_classification',
placeholder: 'image_classification',
required: false,
type: 'text'
},
{
name: 'HUGGINGFACE_ACCESS_TOKEN',
description: `Secret for sending requests to the Hugging Face API. <a class="u-bold" target="_blank" href="https://huggingface.co/docs/api-inference/en/quicktour#get-your-api-token">Learn more</a>.`,
placeholder: 'hf_MUvn...',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'object-detection-with-huggingface',
name: 'Object detection',
tagline: 'Detect objects in images using the Hugging Face inference API.',
permissions: ['any'],
events: ['buckets.*.files.*.create'],
cron: '',
timeout: 15,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install && npm run setup',
'src/main.js',
'node/object-detection-with-huggingface'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/object-detection-with-huggingface">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to authenticate against Appwrite's server APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/advanced/platform/api-keys">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/references#api">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false,
type: 'url'
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/databases">Learn more</a>.`,
value: 'ai',
placeholder: 'ai',
required: false,
type: 'text'
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/collections">Learn more</a>.`,
value: 'object_detection',
placeholder: 'object_detection',
required: false,
type: 'text'
},
{
name: 'APPWRITE_BUCKET_ID',
description: `The ID of the bucket where the images are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/storage/buckets">Learn more</a>.`,
value: 'object_detection',
placeholder: 'object_detection',
required: false,
type: 'text'
},
{
name: 'HUGGINGFACE_ACCESS_TOKEN',
description: `Secret for sending requests to the Hugging Face API. <a class="u-bold" target="_blank" href="https://huggingface.co/docs/api-inference/en/quicktour#get-your-api-token">Learn more</a>.`,
placeholder: 'hf_MUvn...',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'speech-recognition-with-huggingface',
name: 'Speech recognition',
tagline: 'Transcribe audio to text using the Hugging Face inference API.',
permissions: ['any'],
events: ['buckets.*.files.*.create'],
cron: '',
timeout: 15,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install && npm run setup',
'src/main.js',
'node/speech-recognition-with-huggingface'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/speech-recognition-with-huggingface">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to authenticate against Appwrite's server APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/advanced/platform/api-keys">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/references#api">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false,
type: 'url'
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/databases">Learn more</a>.`,
value: 'ai',
placeholder: 'ai',
required: false,
type: 'text'
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/collections">Learn more</a>.`,
value: 'speech_recognition',
placeholder: 'speech_recognition',
required: false,
type: 'text'
},
{
name: 'APPWRITE_BUCKET_ID',
description: `The ID of the bucket where audio is stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/storage/buckets">Learn more</a>.`,
value: 'speech_recognition',
placeholder: 'speech_recognition',
required: false,
type: 'text'
},
{
name: 'HUGGINGFACE_ACCESS_TOKEN',
description: `Secret for sending requests to the Hugging Face API. <a class="u-bold" target="_blank" href="https://huggingface.co/docs/api-inference/en/quicktour#get-your-api-token">Learn more</a>.`,
placeholder: 'hf_MUvn...',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'text-to-speech-with-huggingface',
name: 'Text to speech',
tagline: 'Convert text to speech using the Hugging Face inference API.',
permissions: ['any'],
events: ['databases.*.collections.*.documents.*.create'],
cron: '',
timeout: 15,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install && npm run setup',
'src/main.js',
'node/text-to-speech-with-huggingface'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/text-to-speech-with-huggingface">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'APPWRITE_API_KEY',
description: `The API Key to authenticate against Appwrite's server APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/advanced/platform/api-keys">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/references#api">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false,
type: 'url'
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/databases">Learn more</a>.`,
value: 'ai',
placeholder: 'ai',
required: false,
type: 'text'
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/collections">Learn more</a>.`,
value: 'speech_recognition',
placeholder: 'speech_recognition',
required: false,
type: 'text'
},
{
name: 'APPWRITE_BUCKET_ID',
description: `The ID of the bucket where audio is stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/storage/buckets">Learn more</a>.`,
value: 'speech_recognition',
placeholder: 'speech_recognition',
required: false,
type: 'text'
},
{
name: 'HUGGINGFACE_ACCESS_TOKEN',
description: `Secret for sending requests to the Hugging Face API. <a class="u-bold" target="_blank" href="https://huggingface.co/docs/api-inference/en/quicktour#get-your-api-token">Learn more</a>.`,
placeholder: 'hf_MUvn...',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'generate-with-replicate',
name: 'Generate with Replicate',
tagline: "Generate text, audio and images using Replicate's API.",
permissions: ['any'],
events: [],
cron: '',
timeout: 300,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/generate-with-replicate'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-with-replicate">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'REPLICATE_API_KEY',
description: `A unique key used to authenticate with the Replicate API. <a class="u-bold" target="_blank" href="https://replicate.com/docs/get-started/nodejs">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'chat-with-perplexity-ai',
name: 'Chat with Perplexity AI',
tagline: 'Create a chatbot using the Perplexity AI API.',
permissions: ['any'],
events: [],
cron: '',
timeout: 15,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/chat-with-perplexity-ai'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/chat-with-perplexity-ai">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'PERPLEXITY_API_KEY',
description: `A unique key used to authenticate with the Perplexity API. <a class="u-bold" target="_blank" href="https://docs.perplexity.ai/docs/getting-started">Learn more</a>.`,
placeholder: `pplex-68...999`,
required: true,
type: 'password'
},
{
name: 'PERPLEXITY_MAX_TOKENS',
description: `The maximum number of tokens to generate. <a class="u-bold" target="_blank" href="https://docs.perplexity.ai/docs/getting-started">Learn more</a>.`,
placeholder: `512`,
required: false,
type: 'number'
}
]
},
{
icon: 'icon-chip',
id: 'generate-with-replicate',
name: 'Generate with Replicate',
tagline: "Generate text, audio and images using Replicate's API.",
permissions: ['any'],
events: [],
cron: '',
timeout: 300,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/generate-with-replicate'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-with-replicate">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'REPLICATE_API_KEY',
description: `A unique key used to authenticate with the Replicate API. <a class="u-bold" target="_blank" href="https://replicate.com/docs/get-started/nodejs">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
}
]
},
{
icon: 'icon-chip',
id: 'sync-with-pinecone',
name: 'Sync with Pinecone',
tagline: "Sync your Appwrite database with Pinecone's vector database.",
permissions: ['any'],
events: [],
cron: '',
timeout: 30,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/sync-with-pinecone'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-pinecone">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'OPENAI_API_KEY',
description: `A unique key used to authenticate with the OpenAI API. This is a paid service and you will be charged for each request made to the API. <a class="u-bold" target="_blank" href="https://platform.openai.com/docs/quickstart/add-your-api-key">Learn more</a>.`,
value: '',
placeholder: 'sk-wzG...vcy',
required: true,
type: 'password'
},
{
name: 'PINECONE_API_KEY',
description: `A unique key used to authenticate with the Pinecone API. <a class="u-bold" target="_blank" href="https://docs.pinecone.io/guides/getting-started/authentication">Learn more</a>.`,
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'PINECONE_INDEX_NAME',
description: `The name of the index in Pinecone. <a class="u-bold" target="_blank" href="https://docs.pinecone.io/guides/getting-started/create-index">Learn more</a>.`,
placeholder: 'my-index',
required: true,
type: 'text'
},
{
name: 'APPWRITE_API_KEY',
description: `The API Key to authenticate against Appwrite's server APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/advanced/platform/api-keys">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/references#api">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false,
type: 'url'
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database where the documents are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/databases">Learn more</a>.`,
placeholder: 'my-database',
required: true,
type: 'text'
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection where the documents are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/collections">Learn more</a>.`,
placeholder: 'my-collection',
required: true,
type: 'text'
}
]
},
{
icon: 'icon-chip',
id: 'rag-with-langchain',
name: 'RAG with LangChain',
tagline: 'Generate text using a LangChain RAG model',
permissions: ['any'],
events: [],
cron: '',
timeout: 30,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/rag-with-langchain'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/rag-with-langchain">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'OPENAI_API_KEY',
description: `A unique key used to authenticate with the OpenAI API. This is a paid service and you will be charged for each request made to the API. <a class="u-bold" target="_blank" href="https://platform.openai.com/docs/quickstart/add-your-api-key">Learn more</a>.`,
value: '',
placeholder: 'sk-wzG...vcy',
required: true,
type: 'password'
},
{
name: 'PINECONE_API_KEY',
description: `A unique key used to authenticate with the Pinecone API. <a class="u-bold" target="_blank" href="https://docs.pinecone.io/guides/getting-started/authentication">Learn more</a>.`,
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'PINECONE_INDEX_NAME',
description: `The name of the index in Pinecone. <a class="u-bold" target="_blank" href="https://docs.pinecone.io/guides/getting-started/create-index">Learn more</a>.`,
placeholder: 'my-index',
required: true,
type: 'text'
},
{
name: 'APPWRITE_API_KEY',
description: `The API Key to authenticate against Appwrite's server APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/advanced/platform/api-keys">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/references#api">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false,
type: 'url'
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database where the documents are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/databases">Learn more</a>.`,
placeholder: 'my-database',
required: true,
type: 'text'
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection where the documents are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/collections">Learn more</a>.`,
placeholder: 'my-collection',
required: true,
type: 'text'
}
]
},
{
icon: 'icon-chip',
id: 'speak-with-elevenlabs',
name: 'Speak with ElevenLabs',
tagline: 'Convert text to speech using the ElevenLabs API.',
permissions: ['any'],
cron: '',
events: [],
timeout: 15,
usecases: ['AI'],
runtimes: [
...getRuntimes(
TemplateRuntimes.NODE,
'npm install',
'src/main.js',
'node/speak-with-elevenlabs'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/speak-with-elevenlabs">file</a>.`,
vcsProvider: 'github',
providerRepositoryId: 'templates',
providerOwner: 'appwrite',
providerBranch: 'main',
variables: [
{
name: 'ELEVENLABS_API_KEY',
description: `A unique key used to authenticate with the ElevenLabs API. <a class="u-bold" target="_blank" href="https://elevenlabs.io/docs/api-reference/getting-started">Learn more</a>.`,
placeholder: 'd03xxxxxxxx26',
required: true,
type: 'password'
},
{
name: 'APPWRITE_API_KEY',
description: `The API Key to authenticate against Appwrite's server APIs. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/advanced/platform/api-keys">Learn more</a>.`,
value: '',
placeholder: 'd1efb...aec35',
required: true,
type: 'password'
},
{
name: 'APPWRITE_ENDPOINT',
description: `The URL endpoint of the Appwrite server. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/references#api">Learn more</a>.`,
value: 'https://cloud.appwrite.io/v1',
placeholder: 'https://cloud.appwrite.io/v1',
required: false,
type: 'url'
},
{
name: 'APPWRITE_DATABASE_ID',
description: `The ID of the database where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/databases">Learn more</a>.`,
placeholder: 'my-database',
required: true,
type: 'text'
},
{
name: 'APPWRITE_COLLECTION_ID',
description: `The ID of the collection where the responses are stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/databases/collections">Learn more</a>.`,
placeholder: 'my-collection',
required: true,
type: 'text'
},
{
name: 'APPWRITE_BUCKET_ID',
description: `The ID of the bucket where audio is stored. <a class="u-bold" target="_blank" href="https://appwrite.io/docs/products/storage/buckets">Learn more</a>.`,
placeholder: 'generated_speech',
required: true,
type: 'text'
}
]
}
];
+7 -7
View File
@@ -21,7 +21,8 @@
paymentExpired,
checkForMarkedForDeletion,
checkForMandate,
checkForMissingPaymentMethod
checkForMissingPaymentMethod,
plansInfo
} from '$lib/stores/billing';
import { goto } from '$app/navigation';
import { CommandCenter, registerCommands, registerSearchers } from '$lib/commandCenter';
@@ -37,8 +38,7 @@
import { stripe } from '$lib/stores/stripe';
import MobileSupportModal from './wizard/support/mobileSupportModal.svelte';
import { showSupportModal } from './wizard/support/store';
import ExcesLimitModal from './organization-[organization]/excesLimitModal.svelte';
import { showExcess } from './organization-[organization]/store';
import UsageRates from './wizard/cloudOrganization/usageRates.svelte';
import { activeHeaderAlert, consoleVariables } from './store';
import { headerAlert } from '$lib/stores/headerAlert';
@@ -261,11 +261,14 @@
await checkForUsageLimit(org);
checkForMarkedForDeletion(org);
if (org?.billingPlan !== BillingPlan.STARTER) {
calculateTrialDay(org);
await paymentExpired(org);
await checkPaymentAuthorizationRequired(org);
await checkForMandate(org);
if ($plansInfo.get(org.billingPlan)?.trialDays) {
calculateTrialDay(org);
}
}
$activeHeaderAlert = headerAlert.get();
}
});
@@ -316,9 +319,6 @@
<MobileSupportModal bind:show={$showSupportModal}></MobileSupportModal>
{/if}
{#if isCloud && $showExcess}
<ExcesLimitModal bind:show={$showExcess}></ExcesLimitModal>
{/if}
{#if isCloud && $showUsageRatesModal}
<UsageRates bind:show={$showUsageRatesModal} tier={$organization?.billingPlan} />
{/if}
@@ -17,7 +17,7 @@
import { Pill } from '$lib/elements';
import type { Models } from '@appwrite.io/console';
import type { Organization } from '$lib/stores/organization';
import { daysLeftInTrial } from '$lib/stores/billing';
import { daysLeftInTrial, plansInfo } from '$lib/stores/billing';
import { tooltip } from '$lib/actions/tooltip';
import { toLocaleDate } from '$lib/helpers/date';
import { wizard } from '$lib/stores/wizard';
@@ -82,7 +82,7 @@
<Pill>FREE</Pill>
</div>
{/if}
{#if organization?.billingTrialStartDate && $daysLeftInTrial > 0 && organization.billingPlan !== BillingPlan.STARTER}
{#if organization?.billingTrialStartDate && $daysLeftInTrial > 0 && organization.billingPlan !== BillingPlan.STARTER && $plansInfo.get(organization.billingPlan)?.trialDays}
<div
class="u-flex u-cross-center"
use:tooltip={{
@@ -22,6 +22,9 @@
import { BillingPlan } from '$lib/constants';
import RetryPaymentModal from './retryPaymentModal.svelte';
import { selectedInvoice, showRetryModal } from './store';
import { Button } from '$lib/elements/forms';
export let data;
$: defaultPaymentMethod = $paymentMethods?.paymentMethods?.find(
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
@@ -76,13 +79,27 @@
<Container>
{#if $failedInvoice}
<Alert type="error" class="common-section">
<svelte:fragment slot="title">
The scheduled payment for {$organization.name} failed
</svelte:fragment>
To avoid service disruptions in organization's your projects, please verify your payment
details and update them if necessary.
</Alert>
{#if $failedInvoice?.lastError}
<Alert type="error" class="common-section">
The scheduled payment for {$organization.name} failed due to following error: {$failedInvoice.lastError}
<svelte:fragment slot="buttons">
<Button
text
on:click={() => {
$selectedInvoice = $failedInvoice;
$showRetryModal = true;
}}>Try again</Button>
</svelte:fragment>
</Alert>
{:else}
<Alert type="error" class="common-section">
<svelte:fragment slot="title">
The scheduled payment for {$organization.name} failed
</svelte:fragment>
To avoid service disruptions in organization's your projects, please verify your payment
details and update them if necessary.
</Alert>
{/if}
{/if}
{#if defaultPaymentMethod?.failed && !backupPaymentMethod}
<Alert type="error" class="common-section">
@@ -95,9 +112,8 @@
{/if}
{#if $organization?.billingPlanDowngrade}
<Alert type="info" class="common-section">
Your organization will change to a Starter plan once your current billing cycle ends on {toLocaleDate(
$organization.billingNextInvoiceDate
)}.
Your organization will change to a Starter plan once your current billing cycle ends and
your invoice is paid on {toLocaleDate($organization.billingNextInvoiceDate)}.
</Alert>
{/if}
<div class="common-section">
@@ -106,7 +122,7 @@
<PlanSummary />
<PaymentHistory />
<PaymentMethods />
<BillingAddress />
<BillingAddress billingAddress={data.billingAddress} />
<TaxId />
<BudgetCap />
{#if $organization?.billingPlan !== BillingPlan.STARTER && !!$organization?.billingBudget}
@@ -1,4 +1,6 @@
import { Dependencies } from '$lib/constants';
import type { Address } from '$lib/sdk/billing';
import type { Organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import type { PageLoad } from './$types';
@@ -10,9 +12,19 @@ export const load: PageLoad = async ({ parent, depends }) => {
depends(Dependencies.INVOICES);
depends(Dependencies.ADDRESS);
let billingAddress: Address = null;
const billingAddressId = (organization as Organization)?.billingAddressId;
if (billingAddressId) {
billingAddress = await sdk.forConsole.billing.getOrganizationBillingAddress(
organization.$id,
billingAddressId
);
}
return {
paymentMethods: await sdk.forConsole.billing.listPaymentMethods(),
addressList: await sdk.forConsole.billing.listAddresses(),
aggregationList: await sdk.forConsole.billing.listAggregation(organization.$id)
aggregationList: await sdk.forConsole.billing.listAggregation(organization.$id),
billingAddress
};
};
@@ -56,10 +56,8 @@
request();
}
$: balance = creditList?.credits?.reduce(
(acc: number, curr: Credit) => acc + curr.creditsRemaining,
0
);
$: balance =
creditList?.credits?.reduce((acc: number, curr: Credit) => acc + curr.credits, 0) ?? 0;
$: {
if (reloadOnWizardClose && !$wizard.show) {
@@ -115,7 +113,7 @@
{toLocaleDate(credit.expiration)}
</TableCellText>
<TableCellText title="amount">
{formatCurrency(credit.credits)}
{formatCurrency(credit.total)}
</TableCellText>
</TableRow>
{/each}
@@ -5,6 +5,7 @@
import DropListItem from '$lib/components/dropListItem.svelte';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import type { Address } from '$lib/sdk/billing';
import { addressList } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { organization } from '$lib/stores/organization';
@@ -13,6 +14,8 @@
import EditAddressModal from '$routes/console/account/payments/editAddressModal.svelte';
import ReplaceAddress from './replaceAddress.svelte';
export let billingAddress: Address = null;
let showDropdown = false;
let showBillingAddressDropdown = false;
let showCreate = false;
@@ -40,10 +43,6 @@
trackError(error, Submit.OrganizationBillingAddressUpdate);
}
}
$: billingAddress = $addressList?.billingAddresses?.find(
(address) => address.$id === $organization?.billingAddressId
);
</script>
<CardGrid>
@@ -58,11 +58,10 @@
headerDivider={false}
title="Unable to delete payment method">
<p data-private>
The {isBackup ? 'backup' : 'default'} payment method cannot be deleted as
The {isBackup ? 'backup' : 'default'} payment method cannot be removed as
<b>{$organization?.name}</b>
has an upcoming invoice. To proceed with deletion, set a {isBackup
? 'default'
: 'backup'} or add a new {isBackup ? 'backup' : 'default'} payment method.
has an upcoming invoice. To proceed, set a {isBackup ? 'default' : 'backup'} or add a new
{isBackup ? 'backup' : 'default'} payment method.
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
@@ -76,13 +75,13 @@
icon="exclamation"
state="warning"
headerDivider={false}
title="Delete payment method">
title="Remove payment method">
<p data-private>
Are you sure you want to delete the payment method from <b>{$organization?.name}</b>?
Are you sure you want to remove the payment method from <b>{$organization?.name}</b>?
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
<Button secondary submit>Remove</Button>
</svelte:fragment>
</Modal>
{/if}
@@ -22,7 +22,7 @@
} from '$lib/elements/table';
import { toLocaleDate } from '$lib/helpers/date';
import { formatCurrency } from '$lib/helpers/numbers';
import type { InvoiceList } from '$lib/sdk/billing';
import type { Invoice, InvoiceList } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { VARS } from '$lib/system';
import { Query } from '@appwrite.io/console';
@@ -31,6 +31,7 @@
import { selectedInvoice, showRetryModal } from './store';
let showDropdown = [];
let showFailedError = false;
let offset = 0;
let invoiceList: InvoiceList = {
@@ -51,6 +52,11 @@
]);
}
function retryPayment(invoice: Invoice) {
$selectedInvoice = invoice;
$showRetryModal = true;
}
$: if (offset !== null) {
request();
}
@@ -82,14 +88,45 @@
</TableCellText>
<TableCell title="status">
<Pill
danger={status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
success={status === 'paid' || status === 'succeeded'}
warning={status === 'pending'}>
{status === 'requires_authentication' ? 'failed' : status}
</Pill>
{#if invoice?.lastError}
<DropList bind:show={showFailedError}>
<Pill
danger={status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
success={status === 'paid' ||
status === 'succeeded'}
warning={status === 'pending'}
on:click={() => (showFailedError = true)}
button>
{status === 'requires_authentication'
? 'failed'
: status}
</Pill>
<svelte:fragment slot="list">
<li>
The scheduled payment has failed. <Button
link
on:click={() => {
retryPayment(invoice);
showFailedError = false;
}}>Try again</Button
>.
</li>
</svelte:fragment>
</DropList>
{:else}
<Pill
danger={status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
success={status === 'paid' || status === 'succeeded'}
warning={status === 'pending'}>
{status === 'requires_authentication'
? 'failed'
: status}
</Pill>
{/if}
</TableCell>
<TableCellText title="due">
{formatCurrency(invoice.amount)}
@@ -132,8 +169,7 @@
<DropListItem
icon="refresh"
on:click={() => {
$selectedInvoice = invoice;
$showRetryModal = true;
retryPayment(invoice);
showDropdown[i] = !showDropdown[i];
trackEvent(`click_retry_payment`, {
from: 'button',
@@ -77,13 +77,13 @@
$: if ($organization?.backupPaymentMethodId) {
sdk.forConsole.billing
.getPaymentMethod($organization.backupPaymentMethodId)
.getOrganizationPaymentMethod($organization.$id, $organization.backupPaymentMethodId)
.then((res) => (backupPaymentMethod = res));
}
$: if ($organization?.paymentMethodId) {
sdk.forConsole.billing
.getPaymentMethod($organization.paymentMethodId)
.getOrganizationPaymentMethod($organization.$id, $organization.paymentMethodId)
.then((res) => (defaultPaymentMethod = res));
}
@@ -137,7 +137,7 @@
showDelete = true;
showDropdown = false;
}}>
Delete
Remove
</DropListItem>
</svelte:fragment>
</DropList>
@@ -29,7 +29,9 @@
$: currentPlan = $plansInfo?.get($organization?.billingPlan);
$: extraUsage = currentInvoice?.amount - currentPlan?.price;
$: isTrial = new Date($organization?.billingStartDate).getTime() - today.getTime() > 0;
$: isTrial =
new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 &&
$plansInfo.get($organization.billingPlan)?.trialDays;
</script>
{#if $organization}
@@ -5,7 +5,7 @@
import { sdk } from '$lib/stores/sdk';
import { organization } from '$lib/stores/organization';
import { Dependencies } from '$lib/constants';
import { submitStripeCard } from '$lib/stores/stripe';
import { initializeStripe, isStripeInitialized, submitStripeCard } from '$lib/stores/stripe';
import { onMount } from 'svelte';
import type { PaymentList } from '$lib/sdk/billing';
import { addNotification } from '$lib/stores/notifications';
@@ -24,10 +24,18 @@
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
selectedPaymentMethodId = methods?.total ? methods.paymentMethods[0].$id : null;
} else {
selectedPaymentMethodId = isBackup
? $organization.backupPaymentMethodId
: $organization.paymentMethodId;
// If the selected payment method does not belong to the current user, select the first one.
if (
methods?.total &&
!methods.paymentMethods.some((method) => method.$id === selectedPaymentMethodId)
) {
selectedPaymentMethodId = methods.paymentMethods[0].$id;
}
}
selectedPaymentMethodId = isBackup
? $organization.backupPaymentMethodId
: $organization.paymentMethodId;
});
async function handleSubmit() {
@@ -82,6 +90,14 @@
$: filteredMethods = methods?.paymentMethods.filter((method) => !!method?.last4);
$: if (selectedPaymentMethodId === null && !$isStripeInitialized) {
initializeStripe();
}
$: if (selectedPaymentMethodId) {
isStripeInitialized.set(false);
}
$: if (!show) {
selectedPaymentMethodId = null;
}
@@ -30,8 +30,21 @@
let paymentMethodId: string;
let setAsDefault = false;
onMount(() => {
paymentMethodId = $organization.paymentMethodId;
onMount(async () => {
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
paymentMethodId = $paymentMethods?.total ? $paymentMethods.paymentMethods[0].$id : null;
} else {
paymentMethodId = $organization.paymentMethodId
? $organization.paymentMethodId
: $organization.backupPaymentMethodId;
// If the selected payment method does not belong to the current user, select the first one.
if (
$paymentMethods?.total &&
!$paymentMethods.paymentMethods.some((method) => method.$id === paymentMethodId)
) {
paymentMethodId = $paymentMethods.paymentMethods[0].$id;
}
}
});
async function handleSubmit() {
@@ -68,4 +68,4 @@
});
</script>
<Wizard title="Create project" steps={stepsComponents} on:finish={create} on:exit={onFinish} />
<Wizard title="Create project" steps={stepsComponents} finalMethod={create} on:exit={onFinish} />
@@ -17,7 +17,13 @@
import { toLocaleDate } from '$lib/helpers/date';
import { isTabSelected } from '$lib/helpers/load';
import { Cover } from '$lib/layout';
import { daysLeftInTrial, getServiceLimit, readOnly, tierToPlan } from '$lib/stores/billing';
import {
daysLeftInTrial,
getServiceLimit,
plansInfo,
readOnly,
tierToPlan
} from '$lib/stores/billing';
import {
members,
newMemberModal,
@@ -105,7 +111,7 @@
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER}
<Pill>FREE</Pill>
{/if}
{#if isCloud && $organization?.billingTrialStartDate && $daysLeftInTrial > 0 && $organization.billingPlan !== BillingPlan.STARTER}
{#if isCloud && $organization?.billingTrialStartDate && $daysLeftInTrial > 0 && $organization.billingPlan !== BillingPlan.STARTER && $plansInfo.get($organization.billingPlan)?.trialDays}
<div
class="u-flex u-cross-center"
use:tooltip={{
@@ -1,6 +1,5 @@
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
import { derived } from 'svelte/store';
export const showExcess = writable<boolean>(false);
export const projects = derived(page, ($page) => $page.data?.projects as Models.ProjectList);
@@ -107,7 +107,8 @@
<Heading tag="h6" size="7">Bandwidth</Heading>
<p class="text">
Calculated for all bandwidth used across all projects in your organization.
Calculated for all bandwidth used across all projects in your organization. Resets at
the start of each billing cycle.
</p>
<svelte:fragment slot="aside">
@@ -24,20 +24,17 @@
const sdkCreateVariable = async (key: string, value: string) => {
await sdk.forProject.functions.createVariable($func.$id, key, value);
await invalidate(Dependencies.VARIABLES);
await invalidate(Dependencies.FUNCTION);
await Promise.all([invalidate(Dependencies.VARIABLES), invalidate(Dependencies.FUNCTION)]);
};
const sdkUpdateVariable = async (variableId: string, key: string, value: string) => {
await sdk.forProject.functions.updateVariable($func.$id, variableId, key, value);
await invalidate(Dependencies.VARIABLES);
await invalidate(Dependencies.FUNCTION);
await Promise.all([invalidate(Dependencies.VARIABLES), invalidate(Dependencies.FUNCTION)]);
};
const sdkDeleteVariable = async (variableId: string) => {
await sdk.forProject.functions.deleteVariable($func.$id, variableId);
await invalidate(Dependencies.VARIABLES);
await invalidate(Dependencies.FUNCTION);
await Promise.all([invalidate(Dependencies.VARIABLES), invalidate(Dependencies.FUNCTION)]);
};
</script>
@@ -2,10 +2,11 @@ import { page } from '$app/stores';
import { derived } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
export const runtimesList = derived(
page,
async ($page) => (await $page.data.runtimesList) as Models.RuntimeList
);
export const runtimesList = derived(page, async ($page) => {
const { runtimes } = (await $page.data.runtimesList) as Models.RuntimeList;
const filtered = runtimes.filter((runtime) => !runtime.$id.startsWith('python-ml'));
return { runtimes: filtered, total: filtered.length };
});
export const baseRuntimesList = derived(runtimesList, async ($runtimesList) => {
const baseRuntimes = new Map<string, Models.Runtime>();
@@ -104,7 +104,10 @@
</div>
<CardGrid>
<Heading tag="h4" size="7">Bandwidth</Heading>
<p class="text">Calculated for all bandwidth used in your project.</p>
<p class="text">
Calculated for all bandwidth used across your project. Resets at the start of each
billing cycle.
</p>
<svelte:fragment slot="aside">
{#if network}
{@const humanized = humanFileSize(total(network))}
@@ -29,7 +29,7 @@
async function create() {
try {
await uploader.uploadFile(
uploader.uploadFile(
bucketId,
$createFile.id ?? ID.unique(),
$createFile.files[0],
@@ -42,7 +42,7 @@
addNotification({
type: 'success',
message: `File has been uploaded`
message: `File upload in progress`
});
trackEvent(Submit.FileCreate, {
customId: !!$createFile.id
@@ -1,105 +0,0 @@
<script lang="ts">
import { Button, FormList, InputFile } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { Modal, CustomId, Heading, Alert } from '$lib/components';
import { createEventDispatcher } from 'svelte';
import { page } from '$app/stores';
import { uploader } from '$lib/stores/uploader';
import { bucket } from './store';
import { Permissions } from '$lib/components/permissions';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { ID } from '@appwrite.io/console';
export let showCreate = false;
const bucketId = $page.params.bucket;
const dispatch = createEventDispatcher();
let files: FileList;
let permissions: string[] = [];
let id: string = null;
let error: string;
let showCustomId = false;
async function create() {
try {
showCreate = false;
await uploader.uploadFile(bucketId, id ?? ID.unique(), files[0], permissions);
files = null;
showCustomId = false;
dispatch('created');
addNotification({
type: 'success',
message: `File has been uploaded`
});
trackEvent(Submit.FileCreate, {
customId: !!id
});
} catch (e) {
error = e.message;
trackError(e, Submit.FileCreate);
}
}
$: if (!showCreate) {
id = files = error = null;
permissions = [];
}
</script>
<Modal title="Create file" size="big" {error} bind:show={showCreate} onSubmit={create}>
<FormList>
<div>
<InputFile
bind:files
allowedFileExtensions={$bucket.allowedFileExtensions}
maxSize={$bucket.maximumFileSize} />
</div>
{#if !showCustomId}
<div>
<Pill button on:click={() => (showCustomId = !showCustomId)}>
<span class="icon-pencil" aria-hidden="true" /><span class="text">
File ID
</span>
</Pill>
</div>
{:else}
<CustomId bind:show={showCustomId} name="File" bind:id />
{/if}
<Heading tag="h6" size="7">Permissions</Heading>
<p class="text">
Choose who can access your buckets and files. For more information, visit our
<a
href="https://appwrite.io/docs/advanced/platform/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">
Permissions guide
</a>.
</p>
{#if $bucket.fileSecurity}
<div class="common-section">
<Alert type="info">
<svelte:fragment slot="title">File security enabled</svelte:fragment>
Users will be able to access this file if they have been granted
<b>either File or Bucket permissions</b>.
</Alert>
</div>
<div class="common-section">
<Permissions bind:permissions />
</div>
{:else}
<Alert type="info">
<svelte:fragment slot="title">File security disabled</svelte:fragment>
If you want to assign file permissions, navigate to Bucket settings and enable file security.
Otherwise, only Bucket permissions will be used.
</Alert>
{/if}
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button submit>Create</Button>
</svelte:fragment>
</Modal>
@@ -138,9 +138,11 @@
selectedVar = null;
showPromoteModal = false;
await invalidate(Dependencies.FUNCTION);
await invalidate(Dependencies.VARIABLES);
await invalidate(Dependencies.PROJECT_VARIABLES);
await Promise.all([
invalidate(Dependencies.FUNCTION),
invalidate(Dependencies.VARIABLES),
invalidate(Dependencies.PROJECT_VARIABLES)
]);
addNotification({
type: 'success',
@@ -48,12 +48,18 @@
$: if ($createOrganization.billingPlan === BillingPlan.STARTER) {
$createOrganizationFinalAction = 'Create organization';
}
$: if (plan?.trialDays) {
$createOrganizationFinalAction = 'Start trial';
}
</script>
<WizardStep beforeSubmit={handleBefore}>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">
Confirm the details of your new organization and start your free trial.
Confirm the details of your new organization{!plan.trialDays
? '.'
: ' and start your free trial.'}
</svelte:fragment>
<p class="body-text-1 u-bold">Organization name</p>
@@ -123,9 +129,9 @@
{#if $createOrganization.billingPlan !== BillingPlan.STARTER}
<p class="text u-margin-block-start-16">
This amount, and any additional usage fees, will be charged on a recurring 30-day
billing cycle after your trial period ends on <b
>{toLocaleDate(billingPayDate.toString())}</b
>.
billing cycle{!plan.trialDays
? ''
: ` after your trial period ends on ${toLocaleDate(billingPayDate.toString())}`}.
</p>
{/if}
</Box>
@@ -108,7 +108,9 @@
{tierPro.description}
</p>
</div>
<Pill>14 DAY FREE TRIAL</Pill>
{#if proPlan?.trialDays}
<Pill>14 DAY FREE TRIAL</Pill>
{/if}
</svelte:fragment>
</LabelCard>
</li>
@@ -131,7 +133,6 @@
</p>
</div>
<div class:u-opacity-50={disabled}>
<!-- <Pill>14 DAY FREE TRIAL</Pill> -->
<Pill disabled>COMING SOON</Pill>
</div>
</svelte:fragment>
@@ -10,7 +10,7 @@
import { initializeStripe, isStripeInitialized, submitStripeCard } from '$lib/stores/stripe';
import { sdk } from '$lib/stores/sdk';
import { toLocaleDate } from '$lib/helpers/date';
import { showUsageRatesModal } from '$lib/stores/billing';
import { plansInfo, showUsageRatesModal } from '$lib/stores/billing';
import { PaymentBoxes } from '$lib/components/billing';
const today = new Date();
@@ -74,9 +74,11 @@
<WizardStep beforeSubmit={handleSubmit}>
<svelte:fragment slot="title">Payment details</svelte:fragment>
<svelte:fragment slot="subtitle">
Add a payment method to your organization. You will not be charged until your trial ends on <b
>{toLocaleDate(billingPayDate.toString())}</b
>.
Add a payment method to your organization. {#if $plansInfo.get($createOrganization.billingPlan)?.trialDays}You
will not be charged until your trial ends on <b
>{toLocaleDate(billingPayDate.toString())}</b
>.
{/if}
</svelte:fragment>
<FormList class="u-margin-block-start-8">
@@ -1,10 +1,10 @@
import { BillingPlan } from '$lib/constants';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import type { Tier } from '$lib/stores/billing';
import { type Tier } from '$lib/stores/billing';
import { writable } from 'svelte/store';
export const createOrgSteps = writable<WizardStepsType>(new Map());
export const createOrganizationFinalAction = writable<string>('Start trial');
export const createOrganizationFinalAction = writable<string>('Create');
export const createOrganization = writable<{
id?: string;
@@ -161,8 +161,8 @@
<div class:u-opacity-50={disabled}>
{#if $organization.billingPlan === BillingPlan.PRO}
<Pill disabled>CURRENT PLAN</Pill>
{:else}
<Pill>14 DAY FREE TRIAL</Pill>
{:else if proPlan?.trialDays}
<Pill>{proPlan?.trialDays} DAY FREE TRIAL</Pill>
{/if}
</div>
</svelte:fragment>
@@ -49,6 +49,10 @@
$: if (!$isUpgrade) {
$changeOrganizationFinalAction = 'Confirm plan change';
}
$: if (plan?.trialDays) {
$changeOrganizationFinalAction = 'Start trial';
}
</script>
{#if downgradeToStarter}
@@ -77,7 +81,11 @@
<WizardStep>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">
Confirm the details of your new organization and start your free trial.
Confirm the details of your new organization{!$plansInfo.get(
$changeOrganizationTier.billingPlan
)?.trialDays
? '.'
: ' and start your free trial.'}
</svelte:fragment>
<p class="body-text-1 u-bold">Organization name</p>
@@ -144,9 +152,9 @@
<p class="text u-margin-block-start-16">
This amount, and any additional usage fees, will be charged on a recurring 30-day
billing cycle after your trial period ends on <b
>{toLocaleDate(billingPayDate.toString())}</b
>.
billing cycle{!$plansInfo.get($changeOrganizationTier.billingPlan)?.trialDays
? ''
: ` after your trial period ends on ${toLocaleDate(billingPayDate.toString())}`}.
</p>
</Box>
</WizardStep>
@@ -1,11 +1,11 @@
import { BillingPlan } from '$lib/constants';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import type { Tier } from '$lib/stores/billing';
import { type Tier } from '$lib/stores/billing';
import { writable } from 'svelte/store';
export const changeTierSteps = writable<WizardStepsType>(new Map());
export const isUpgrade = writable<boolean>(false);
export const changeOrganizationFinalAction = writable<string>('Start trial');
export const changeOrganizationFinalAction = writable<string>('Create');
export const changeOrganizationTier = writable<{
billingPlan: Tier;