mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #2722 from appwrite/billing-sdk-refactor
This commit is contained in:
@@ -24,9 +24,15 @@ export async function createFreeProject(page: Page): Promise<Metadata> {
|
||||
const regionPicker = dialog.locator('button[role="combobox"]');
|
||||
if (await regionPicker.isVisible()) {
|
||||
await regionPicker.click();
|
||||
await page.getByRole('option', { name: /New York/i }).click();
|
||||
const firstEnabledOption = page
|
||||
.locator('[role="option"]:not([data-disabled="true"])')
|
||||
.first();
|
||||
|
||||
region = 'nyc';
|
||||
if ((await firstEnabledOption.count()) > 0) {
|
||||
const selectedRegion = await firstEnabledOption.getAttribute('data-value');
|
||||
await firstEnabledOption.click();
|
||||
region = selectedRegion?.replace(/"/g, '') || 'fra';
|
||||
}
|
||||
}
|
||||
|
||||
await dialog.getByRole('button', { name: 'create' }).click();
|
||||
|
||||
@@ -57,9 +57,15 @@ export async function createProProject(page: Page): Promise<Metadata> {
|
||||
const regionPicker = dialog.locator('button[role="combobox"]');
|
||||
if (await regionPicker.isVisible()) {
|
||||
await regionPicker.click();
|
||||
await page.getByRole('option', { name: /New York/i }).click();
|
||||
const firstEnabledOption = page
|
||||
.locator('[role="option"]:not([data-disabled="true"])')
|
||||
.first();
|
||||
|
||||
region = 'nyc';
|
||||
if ((await firstEnabledOption.count()) > 0) {
|
||||
const selectedRegion = await firstEnabledOption.getAttribute('data-value');
|
||||
await firstEnabledOption.click();
|
||||
region = selectedRegion?.replace(/"/g, '') || 'fra';
|
||||
}
|
||||
}
|
||||
|
||||
await dialog.getByRole('button', { name: 'create' }).click();
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Searcher } from '../commands';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Platform, Query } from '@appwrite.io/console';
|
||||
import { getTeamOrOrganizationList } from '$lib/stores/organization';
|
||||
|
||||
export const orgSearcher = (async (query: string) => {
|
||||
const { teams } = !isCloud
|
||||
? await sdk.forConsole.teams.list()
|
||||
: await sdk.forConsole.billing.listOrganization([
|
||||
Query.equal('platform', Platform.Appwrite)
|
||||
]);
|
||||
const { teams } = await getTeamOrOrganizationList();
|
||||
|
||||
return teams
|
||||
.filter((organization) => organization.name.toLowerCase().includes(query.toLowerCase()))
|
||||
@@ -18,7 +12,11 @@ export const orgSearcher = (async (query: string) => {
|
||||
return {
|
||||
label: organization.name,
|
||||
callback: () => {
|
||||
goto(`${base}/organization-${organization.$id}`);
|
||||
goto(
|
||||
resolve('/(console)/organization-[organization]', {
|
||||
organization: organization.$id
|
||||
})
|
||||
);
|
||||
},
|
||||
group: 'organizations'
|
||||
} as const;
|
||||
|
||||
@@ -38,23 +38,21 @@
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
// props
|
||||
interface Props {
|
||||
currentPlan: Models.BillingPlan;
|
||||
organization: Models.Organization;
|
||||
projectsToArchive: Models.Project[];
|
||||
organization: Organization;
|
||||
currentPlan: Plan;
|
||||
archivedTotalOverall: number;
|
||||
archivedOffset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
let {
|
||||
projectsToArchive,
|
||||
organization,
|
||||
currentPlan,
|
||||
organization,
|
||||
projectsToArchive,
|
||||
archivedTotalOverall,
|
||||
archivedOffset,
|
||||
limit
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { hideNotification } from '$lib/helpers/notifications';
|
||||
import { backupsBannerId, showPolicyAlert } from '$lib/stores/database';
|
||||
import { IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
@@ -18,14 +17,16 @@
|
||||
</script>
|
||||
|
||||
{#if $showPolicyAlert && isCloud && $organization?.$id && page.url.pathname.match(/\/databases\/database-[^/]+$/)}
|
||||
{@const isFreePlan = $organization?.billingPlan === BillingPlan.FREE}
|
||||
{@const areBackupsAvailable = $organization?.billingPlanDetails.backupsEnabled}
|
||||
|
||||
{@const subtitle = isFreePlan
|
||||
{@const subtitle = !areBackupsAvailable
|
||||
? 'Upgrade your plan to ensure your data stays safe and backed up'
|
||||
: 'Protect your data by quickly adding a backup policy'}
|
||||
|
||||
{@const ctaText = isFreePlan ? 'Upgrade plan' : 'Create policy'}
|
||||
{@const ctaURL = isFreePlan ? $upgradeURL : `${page.url.pathname}/backups`}
|
||||
{@const ctaText = !areBackupsAvailable ? 'Upgrade plan' : 'Create policy'}
|
||||
{@const ctaURL = !areBackupsAvailable
|
||||
? getChangePlanUrl($organization.$id)
|
||||
: `${page.url.pathname}/backups`}
|
||||
|
||||
<HeaderAlert type="warning" title="Your database has no backup policy">
|
||||
<svelte:fragment>{subtitle}</svelte:fragment>
|
||||
@@ -35,7 +36,7 @@
|
||||
href={ctaURL}
|
||||
secondary
|
||||
fullWidthMobile
|
||||
event={isFreePlan ? 'backup_banner_upgrade' : 'backup_banner_add'}>
|
||||
event={!areBackupsAvailable ? 'backup_banner_upgrade' : 'backup_banner_add'}>
|
||||
<span class="text">{ctaText}</span>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { isCloud, isSelfHosted } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -125,8 +125,8 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// fast path: don't subscribe if org is on a free plan or is self-hosted.
|
||||
if (isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE)) return;
|
||||
// fast path: don't subscribe if org doesn't support backups or is self-hosted.
|
||||
if (isSelfHosted || (isCloud && !$organization?.billingPlanDetails.backupsEnabled)) return;
|
||||
|
||||
return realtime.forProject(page.params.region, 'console', (response) => {
|
||||
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
|
||||
|
||||
@@ -2,21 +2,19 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes, readOnly, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { hideBillingHeaderRoutes, readOnly, getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
</script>
|
||||
|
||||
{#if $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
{#if $organization?.$id && !$organization?.billingPlanDetails.usage && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`${$organization.name} usage has reached the ${tierToPlan($organization.billingPlan).name} plan limit`}>
|
||||
title={`${$organization.name} usage has reached the ${$organization.billingPlanDetails.name} plan limit`}>
|
||||
<svelte:fragment>
|
||||
Usage for the <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name}
|
||||
Usage for the <b>{$organization.name}</b> organization has reached the limits of the {$organization
|
||||
.billingPlanDetails.name}
|
||||
plan. Consider upgrading to increase your resource usage.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
@@ -29,7 +27,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl($organization.$id)}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
import { orgMissingPaymentMethod } from '$routes/(console)/store';
|
||||
|
||||
// exists
|
||||
$: hasOrgBillingContext = !!$orgMissingPaymentMethod;
|
||||
|
||||
// needs any methods
|
||||
$: requiresPaymentMethod =
|
||||
hasOrgBillingContext && $orgMissingPaymentMethod.billingPlanDetails.requiresPaymentMethod;
|
||||
|
||||
// has any methods
|
||||
$: hasAnyPaymentMethod =
|
||||
hasOrgBillingContext &&
|
||||
(!!$orgMissingPaymentMethod.paymentMethodId ||
|
||||
!!$orgMissingPaymentMethod.backupPaymentMethodId);
|
||||
|
||||
// is url excluded
|
||||
$: isBillingHeaderHidden = hideBillingHeaderRoutes.includes(page.url.pathname);
|
||||
|
||||
// should show header
|
||||
$: shouldShowBillingHeader =
|
||||
hasOrgBillingContext &&
|
||||
requiresPaymentMethod &&
|
||||
!hasAnyPaymentMethod &&
|
||||
!isBillingHeaderHidden;
|
||||
</script>
|
||||
|
||||
{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
{#if shouldShowBillingHeader}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`Payment method required for ${$orgMissingPaymentMethod.name}`}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { activeHeaderAlert } from '$routes/(console)/store';
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes(base + '/account')}
|
||||
{#if show && $organization?.$id && !$organization?.billingPlanDetails.supportsCredits && !page.url.pathname.includes(base + '/account')}
|
||||
<GradientBanner on:close={handleClose}>
|
||||
<Layout.Stack
|
||||
gap="m"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Click } from '$lib/actions/analytics';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes, upgradeURL } from '$lib/stores/billing';
|
||||
import { hideBillingHeaderRoutes, getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
import SelectProjectCloud from './selectProjectCloud.svelte';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<SelectProjectCloud bind:showSelectProject bind:selectedProjects {organizationId} />
|
||||
|
||||
{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
{#if organizationId && $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert
|
||||
type="warning"
|
||||
title="Action required: You have more than {$currentPlan.projects} projects.">
|
||||
@@ -38,7 +38,7 @@
|
||||
showSelectProject = true;
|
||||
}}>Manage projects</Button>
|
||||
<Button
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl(organizationId)}
|
||||
event={Click.OrganizationClickUpgrade}
|
||||
eventData={{
|
||||
from: 'button',
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
|
||||
async function updateSelected() {
|
||||
try {
|
||||
await sdk.forConsole.billing.updateSelectedProjects(
|
||||
projects[0].teamId,
|
||||
selectedProjects
|
||||
);
|
||||
await sdk.forConsole.organizations.updateProjects({
|
||||
organizationId,
|
||||
projects: selectedProjects
|
||||
});
|
||||
|
||||
showSelectProject = false;
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Button, 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 { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let required = false;
|
||||
export let coupon: string = '';
|
||||
export let couponData: Partial<Coupon> = {
|
||||
export let couponData: Partial<Models.Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
@@ -18,8 +18,10 @@
|
||||
|
||||
async function addCoupon() {
|
||||
try {
|
||||
const response = await sdk.forConsole.billing.getCouponAccount(coupon);
|
||||
couponData = response;
|
||||
couponData = await sdk.forConsole.account.getCoupon({
|
||||
couponId: coupon
|
||||
});
|
||||
|
||||
dispatch('validation', couponData);
|
||||
coupon = null;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { IconTag, IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Badge, Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let couponData: Partial<Coupon> = {
|
||||
export let couponData: Partial<Models.Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { IconTag } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Badge, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let label: string;
|
||||
export let value: number;
|
||||
export let couponData: Partial<Coupon> = {
|
||||
export let couponData: Partial<Models.Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Card } from '..';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlanGroup } from '@appwrite.io/console';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { getBasePlanFromGroup, getChangePlanUrl } from '$lib/stores/billing';
|
||||
|
||||
export let service: string;
|
||||
export let eventSource: string;
|
||||
let {
|
||||
service,
|
||||
eventSource,
|
||||
organizationId = null,
|
||||
children = null
|
||||
}: {
|
||||
service: string;
|
||||
eventSource: string;
|
||||
organizationId?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
const proPlanName = getBasePlanFromGroup(BillingPlanGroup.Pro).name;
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<slot>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<Layout.Stack alignItems="center">
|
||||
<Typography.Text variant="m-600">Upgrade to add {service}</Typography.Text>
|
||||
<Typography.Text>
|
||||
Upgrade to a {tierToPlan(BillingPlan.PRO).name} plan to add {service} to your organization
|
||||
Upgrade to a {proPlanName} plan to add {service} to your organization
|
||||
</Typography.Text>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
fullWidthMobile
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl(organizationId)}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
@@ -31,5 +45,5 @@
|
||||
Upgrade
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</slot>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { InputChoice, InputNumber } from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon, Estimation } from '$lib/sdk/billing';
|
||||
import { type Tier } from '$lib/stores/billing';
|
||||
import { Card, Divider, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { CreditsApplied } from '.';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { AppwriteException } from '@appwrite.io/console';
|
||||
import { AppwriteException, type Models } from '@appwrite.io/console';
|
||||
import DiscountsApplied from './discountsApplied.svelte';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let billingPlan: Models.BillingPlan;
|
||||
export let collaborators: string[];
|
||||
export let couponData: Partial<Coupon>;
|
||||
export let couponData: Partial<Models.Coupon>;
|
||||
export let billingBudget: number;
|
||||
export let fixedCoupon = false; // If true, the coupon cannot be removed
|
||||
export let isDowngrade = false;
|
||||
export let organizationId: string | undefined = undefined;
|
||||
|
||||
let budgetEnabled = false;
|
||||
let estimation: Estimation;
|
||||
let estimation: Models.Estimation;
|
||||
|
||||
async function getEstimate(
|
||||
billingPlan: string,
|
||||
@@ -26,11 +24,11 @@
|
||||
couponId: string | undefined
|
||||
) {
|
||||
try {
|
||||
estimation = await sdk.forConsole.billing.estimationCreateOrganization(
|
||||
estimation = await sdk.forConsole.organizations.estimationCreateOrganization({
|
||||
billingPlan,
|
||||
couponId === '' ? null : couponId,
|
||||
collaborators ?? []
|
||||
);
|
||||
invites: collaborators ?? [],
|
||||
couponId: couponId === '' ? null : couponId
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof AppwriteException) {
|
||||
if (
|
||||
@@ -56,12 +54,12 @@
|
||||
couponId: string | undefined
|
||||
) {
|
||||
try {
|
||||
estimation = await sdk.forConsole.billing.estimationUpdatePlan(
|
||||
estimation = await sdk.forConsole.organizations.estimationUpdatePlan({
|
||||
organizationId,
|
||||
billingPlan,
|
||||
couponId && couponId.length > 0 ? couponId : null,
|
||||
collaborators ?? []
|
||||
);
|
||||
billingPlan: billingPlan,
|
||||
invites: collaborators ?? [],
|
||||
couponId: couponId && couponId.length > 0 ? couponId : null
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof AppwriteException) {
|
||||
if (
|
||||
@@ -81,8 +79,8 @@
|
||||
}
|
||||
|
||||
$: organizationId
|
||||
? getUpdatePlanEstimate(organizationId, billingPlan, collaborators, couponData?.code)
|
||||
: getEstimate(billingPlan, collaborators, couponData?.code);
|
||||
? getUpdatePlanEstimate(organizationId, billingPlan.$id, collaborators, couponData?.code)
|
||||
: getEstimate(billingPlan.$id, collaborators, couponData?.code);
|
||||
</script>
|
||||
|
||||
{#if estimation}
|
||||
@@ -105,6 +103,7 @@
|
||||
{#if couponData?.status === 'active'}
|
||||
<CreditsApplied bind:couponData {fixedCoupon} />
|
||||
{/if}
|
||||
|
||||
<Divider />
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Typography.Text>Total due</Typography.Text>
|
||||
|
||||
@@ -6,4 +6,3 @@ export { default as PlanComparisonBox } from './planComparisonBox.svelte';
|
||||
export { default as EmptyCardCloud } from './emptyCardCloud.svelte';
|
||||
export { default as CreditsApplied } from './creditsApplied.svelte';
|
||||
export { default as PlanSelection } from './planSelection.svelte';
|
||||
export { default as SelectPlan } from './selectPlan.svelte';
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
import { CreditCardBrandImage } from '..';
|
||||
import { initializeStripe, unmountPaymentElement } from '$lib/stores/stripe';
|
||||
import { Badge, Card, Layout } from '@appwrite.io/pink-svelte';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import type { PaymentMethod } from '@stripe/stripe-js';
|
||||
import StatePicker from './statePicker.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let methods: PaymentMethodData[];
|
||||
export let methods: Array<Models.PaymentMethod>;
|
||||
export let group: string;
|
||||
export let name: string;
|
||||
export let defaultMethod: string = null;
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { page } from '$app/state';
|
||||
import { Spinner } from '@appwrite.io/pink-svelte';
|
||||
import type { PaymentMethod } from '@stripe/stripe-js';
|
||||
import type { PaymentMethod as StripePaymentMethod } from '@stripe/stripe-js';
|
||||
import StatePicker from './statePicker.svelte';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let onCardSubmit: ((card: PaymentMethodData) => void) | null = null;
|
||||
export let onCardSubmit: ((card: Models.PaymentMethod) => void) | null = null;
|
||||
|
||||
let modal: FakeModal;
|
||||
let name: string;
|
||||
let state: string = '';
|
||||
let error: string = null;
|
||||
let showState: boolean = false;
|
||||
let paymentMethod: PaymentMethod | null = null;
|
||||
let paymentMethod: StripePaymentMethod | null = null;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
@@ -47,15 +47,15 @@
|
||||
|
||||
const card = await submitStripeCard(name, page?.params?.organization ?? null);
|
||||
if (card && Object.hasOwn(card, 'id')) {
|
||||
if ((card as PaymentMethod).card?.country === 'US') {
|
||||
paymentMethod = card as PaymentMethod;
|
||||
if ((card as StripePaymentMethod).card?.country === 'US') {
|
||||
paymentMethod = card as StripePaymentMethod;
|
||||
showState = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
modal.closeModal();
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
onCardSubmit?.(card as PaymentMethodData);
|
||||
onCardSubmit?.(card as Models.PaymentMethod);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'A new payment method has been added to your account'
|
||||
|
||||
@@ -1,103 +1,130 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { page } from '$app/state';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { BillingPlanGroup, type Models } from '@appwrite.io/console';
|
||||
import { Card, Layout, Tabs, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { getBasePlanFromGroup, planHasGroup, plansInfo } from '$lib/stores/billing';
|
||||
|
||||
export let downgrade = false;
|
||||
let {
|
||||
downgrade = false
|
||||
}: {
|
||||
downgrade?: boolean;
|
||||
} = $props();
|
||||
|
||||
let selectedTab: Tier = BillingPlan.FREE;
|
||||
let selectedTab: string = $state(getBasePlanFromGroup(BillingPlanGroup.Starter).$id);
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
const currentPlan: Models.BillingPlan = $derived($plansInfo.get(selectedTab));
|
||||
const visiblePlans: Array<Models.BillingPlan> = $derived.by(() => {
|
||||
return page.data.plans.plans.filter(
|
||||
(plan: Models.BillingPlan) => plan.group !== BillingPlanGroup.Scale
|
||||
);
|
||||
});
|
||||
|
||||
const allTiers: Tier[] = [BillingPlan.FREE, BillingPlan.PRO, BillingPlan.SCALE];
|
||||
$: visibleTiers = allTiers.filter((tier) => tier !== BillingPlan.SCALE);
|
||||
const uniquePlans: Array<Models.BillingPlan> = $derived.by(() => {
|
||||
const map = new Map(visiblePlans.map((p) => [p.group ?? p.$id, p]));
|
||||
|
||||
return [...map.values()];
|
||||
});
|
||||
|
||||
function pluralize(count: number, singular: string, plural?: string) {
|
||||
if (count === 1) return singular;
|
||||
return plural ?? `${singular}s`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Base>
|
||||
<Layout.Stack>
|
||||
<Tabs.Root stretch let:root>
|
||||
{#each visibleTiers as tier}
|
||||
{#each uniquePlans as plan}
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
active={selectedTab === tier}
|
||||
on:click={() => (selectedTab = tier)}>
|
||||
{tierToPlan(tier).name}
|
||||
active={selectedTab === plan.$id}
|
||||
on:click={() => (selectedTab = plan.$id)}>
|
||||
{plan.name}
|
||||
</Tabs.Item.Button>
|
||||
{/each}
|
||||
</Tabs.Root>
|
||||
|
||||
<Typography.Text variant="m-600">{plan.name} plan</Typography.Text>
|
||||
{#if selectedTab === BillingPlan.FREE}
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text"> Limited to 1 organization member </span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{plan.bandwidth}GB bandwidth
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{plan.storage}GB storage
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{formatNum(plan.executions)} executions
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
{:else}
|
||||
<ul class="un-order-list">
|
||||
<li>
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>
|
||||
Limited to {plan.bandwidth}GB bandwidth
|
||||
</li>
|
||||
<li>
|
||||
Limited to {plan.storage}GB storage
|
||||
</li>
|
||||
<li>
|
||||
Limited to {formatNum(plan.executions)} executions
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if selectedTab === BillingPlan.PRO}
|
||||
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>Unlimited seats</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{:else if selectedTab === BillingPlan.SCALE}
|
||||
<Typography.Text>Everything in the Pro plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles</li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
<li>SSO <span class="inline-tag">Coming soon</span></li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
<Typography.Text variant="m-600">{currentPlan.name} plan</Typography.Text>
|
||||
|
||||
{@render appwritePlanView()}
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
{#snippet appwritePlanView()}
|
||||
{#if planHasGroup(selectedTab, BillingPlanGroup.Starter)}
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
Limited to {currentPlan.databases}
|
||||
{pluralize(currentPlan.databases, 'Database')}, {currentPlan.buckets}
|
||||
{pluralize(currentPlan.buckets, 'Bucket')}, {currentPlan.functions}
|
||||
{pluralize(currentPlan.functions, 'Function')} per project
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text"> Limited to 1 organization member </span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{currentPlan.bandwidth}GB bandwidth
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{currentPlan.storage}GB storage
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{formatNum(currentPlan.executions)} executions
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
{:else}
|
||||
<ul class="un-order-list">
|
||||
<li>
|
||||
Limited to {currentPlan.databases}
|
||||
{pluralize(currentPlan.databases, 'Database')}, {currentPlan.buckets}
|
||||
{pluralize(currentPlan.buckets, 'Bucket')}, {currentPlan.functions}
|
||||
{pluralize(currentPlan.functions, 'Function')} per project
|
||||
</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>
|
||||
Limited to {currentPlan.bandwidth}GB bandwidth
|
||||
</li>
|
||||
<li>
|
||||
Limited to {currentPlan.storage}GB storage
|
||||
</li>
|
||||
<li>
|
||||
Limited to {formatNum(currentPlan.executions)} executions
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if planHasGroup(selectedTab, BillingPlanGroup.Pro)}
|
||||
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>Unlimited seats</li>
|
||||
<li>{currentPlan.bandwidth}GB bandwidth</li>
|
||||
<li>{currentPlan.storage}GB storage</li>
|
||||
<li>{formatNum(currentPlan.executions)} executions</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{:else if planHasGroup(selectedTab, BillingPlanGroup.Scale)}
|
||||
<Typography.Text>Everything in the Pro plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles</li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
<li>SSO <span class="inline-tag">Coming soon</span></li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
calculateExcess,
|
||||
plansInfo,
|
||||
tierToPlan,
|
||||
getServiceLimit,
|
||||
type Tier
|
||||
} from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { abbreviateNumber } from '$lib/helpers/numbers';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Alert, Icon, Table, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import type { AggregationTeam } from '$lib/sdk/billing';
|
||||
|
||||
export let tier: Tier;
|
||||
|
||||
const plan = $plansInfo?.get(tier);
|
||||
let excess: {
|
||||
bandwidth?: number;
|
||||
storage?: number;
|
||||
users?: number;
|
||||
executions?: number;
|
||||
members?: number;
|
||||
} = null;
|
||||
let aggregation: AggregationTeam = null;
|
||||
let showExcess = false;
|
||||
|
||||
onMount(async () => {
|
||||
aggregation = await sdk.forConsole.billing.getAggregation(
|
||||
$organization.$id,
|
||||
$organization.billingAggregationId
|
||||
);
|
||||
excess = calculateExcess(aggregation, plan);
|
||||
showExcess = Object.values(excess).some((value) => value > 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if showExcess}
|
||||
<Alert.Inline
|
||||
status="error"
|
||||
title={` Your organization will switch to ${tierToPlan(BillingPlan.FREE).name} plan on ${toLocaleDate(
|
||||
$organization.billingNextInvoiceDate
|
||||
)}`}>
|
||||
You will retain access to {tierToPlan($organization.billingPlan).name} plan features until your
|
||||
billing period ends. After that,
|
||||
{#if excess?.members > 0}<span class="u-bold">
|
||||
all team members except the owner will be removed,</span>
|
||||
{:else}
|
||||
<span class="u-bold">your organization will be limited to Free plan resources,</span>
|
||||
{/if} and service disruptions may occur if usage exceeds Free plan limits.
|
||||
</Alert.Inline>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Resource</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Free limit</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>
|
||||
Excess usage <Tooltip maxWidth="fit-content"
|
||||
><Icon icon={IconInfo} />
|
||||
<span slot="tooltip">Metrics are estimates updated every 24 hours</span>
|
||||
</Tooltip>
|
||||
</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#if excess?.members}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Organization members</Table.Cell>
|
||||
<Table.Cell {root}>{getServiceLimit('members', tier)} members</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<p class="u-color-text-danger u-flex u-cross-center u-gap-4">
|
||||
<span class="icon-arrow-up"></span>
|
||||
{excess?.members} members
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
{#if excess?.storage}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Storage</Table.Cell>
|
||||
<Table.Cell {root}>{plan.storage} GB</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up"></span>
|
||||
{humanFileSize(excess?.storage).value}
|
||||
{humanFileSize(excess?.storage).unit}
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
{#if excess?.executions}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Function executions</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{abbreviateNumber(plan.executions)} executions
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up"></span>
|
||||
<span
|
||||
title={excess?.executions
|
||||
? excess.executions.toString()
|
||||
: 'executions'}>
|
||||
{formatNum(excess?.executions)} executions
|
||||
</span>
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
{#if excess?.users}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Users</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{abbreviateNumber(plan.users)} users
|
||||
</Table.Cell>
|
||||
<Table.Cell column="usage" {root}>
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up"></span>
|
||||
<span title={excess?.users ? excess.users.toString() : 'users'}>
|
||||
{formatNum(excess?.users)} users
|
||||
</span>
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
</Table.Root>
|
||||
{/if}
|
||||
@@ -1,41 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
import { Badge, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { LabelCard } from '..';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { page } from '$app/state';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { billingIdToPlan } from '$lib/stores/billing';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
import { BillingPlanGroup, type Models } from '@appwrite.io/console';
|
||||
import { Badge, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let billingPlan: BillingPlan;
|
||||
export let isNewOrg = false;
|
||||
export let selfService = true;
|
||||
export let anyOrgFree = false;
|
||||
let {
|
||||
isNewOrg = false,
|
||||
selfService = true,
|
||||
anyOrgFree = false,
|
||||
selectedBillingPlan = $bindable()
|
||||
}: {
|
||||
isNewOrg?: boolean;
|
||||
selfService?: boolean;
|
||||
anyOrgFree?: boolean;
|
||||
selectedBillingPlan: Models.BillingPlan;
|
||||
} = $props();
|
||||
|
||||
$: plans = Object.values(page.data.plans.plans) as Plan[];
|
||||
$: currentPlanInList = plans.some((plan) => plan.$id === $currentPlan?.$id);
|
||||
let selectedPlan = $state(selectedBillingPlan.$id);
|
||||
|
||||
// experiment to remove scale plan temporarily
|
||||
$: plansWithoutScale = plans.filter((plan) => plan.$id != BillingPlan.SCALE);
|
||||
const visiblePlans = $derived(Object.values(page.data.plans.plans) as Models.BillingPlan[]);
|
||||
const currentPlanInList = $derived(visiblePlans.some((plan) => plan.$id === $currentPlan?.$id));
|
||||
|
||||
function shouldShowTooltip(plan: Plan) {
|
||||
if (plan.$id !== BillingPlan.FREE) return true;
|
||||
function shouldShowTooltip(plan: Models.BillingPlan) {
|
||||
if (plan.group !== BillingPlanGroup.Starter) return true;
|
||||
else return !anyOrgFree;
|
||||
}
|
||||
|
||||
function shouldDisable(plan: Models.BillingPlan) {
|
||||
return plan.group === BillingPlanGroup.Starter && anyOrgFree;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
selectedBillingPlan = billingIdToPlan(selectedPlan);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Layout.Stack>
|
||||
{#each plansWithoutScale as plan}
|
||||
{#each visiblePlans as plan}
|
||||
<Tooltip disabled={shouldShowTooltip(plan)} maxWidth="fit-content">
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={!selfService || (plan.$id === BillingPlan.FREE && anyOrgFree)}
|
||||
tooltipShow={plan.$id === BillingPlan.FREE && anyOrgFree}
|
||||
bind:group={selectedPlan}
|
||||
disabled={!selfService || shouldDisable(plan)}
|
||||
tooltipShow={shouldDisable(plan)}
|
||||
value={plan.$id}
|
||||
title={plan.name}>
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === plan.$id && !isNewOrg}
|
||||
{#if $organization?.billingPlanId === plan.$id && !isNewOrg}
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
@@ -57,11 +71,11 @@
|
||||
{#if $currentPlan && !currentPlanInList}
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
bind:group={selectedPlan}
|
||||
value={$currentPlan.$id}
|
||||
title={$currentPlan.name}>
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === $currentPlan.$id && !isNewOrg}
|
||||
{#if $organization?.billingPlanId === $currentPlan.$id && !isNewOrg}
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
import PaymentModal from './paymentModal.svelte';
|
||||
@@ -10,15 +9,16 @@
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let value: string;
|
||||
export let taxId = '';
|
||||
export let methods: PaymentList;
|
||||
export let value: string;
|
||||
export let methods: Models.PaymentMethodList;
|
||||
|
||||
let showTaxId = false;
|
||||
let showPaymentModal = false;
|
||||
|
||||
async function cardSaved(card: PaymentMethodData) {
|
||||
async function cardSaved(card: Models.PaymentMethod) {
|
||||
value = card.$id;
|
||||
|
||||
if (value) {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { LabelCard } from '..';
|
||||
|
||||
export let billingPlan: string;
|
||||
export let anyOrgFree = false;
|
||||
export let isNewOrg = false;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
{#if billingPlan}
|
||||
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
|
||||
{#each $plansInfo.values() as plan}
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={(plan.$id === BillingPlan.FREE && anyOrgFree) || !plan.selfService}
|
||||
value={plan.$id}
|
||||
tooltipShow={plan.$id === BillingPlan.FREE && anyOrgFree}
|
||||
tooltipText={plan.$id === BillingPlan.FREE
|
||||
? 'You are limited to 1 Free organization per account.'
|
||||
: ''}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{plan.name}
|
||||
{#if $organization?.billingPlan === plan.$id && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{plan.desc}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(plan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -5,18 +5,18 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { states } from './state';
|
||||
import { Alert, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { CreditCardBrandImage } from '../index.js';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let {
|
||||
show = $bindable(false),
|
||||
paymentMethod
|
||||
}: {
|
||||
show: boolean;
|
||||
paymentMethod: PaymentMethodData;
|
||||
paymentMethod: Models.PaymentMethod;
|
||||
} = $props();
|
||||
|
||||
let selectedState = $state('');
|
||||
@@ -40,12 +40,12 @@
|
||||
error = null;
|
||||
|
||||
try {
|
||||
await sdk.forConsole.billing.setPaymentMethod(
|
||||
paymentMethod.$id,
|
||||
paymentMethod.providerMethodId,
|
||||
paymentMethod.name,
|
||||
selectedState
|
||||
);
|
||||
await sdk.forConsole.account.updatePaymentMethod({
|
||||
paymentMethodId: paymentMethod.$id,
|
||||
expiryMonth: paymentMethod.expiryMonth,
|
||||
expiryYear: paymentMethod.expiryYear,
|
||||
state: selectedState
|
||||
});
|
||||
trackEvent(Submit.PaymentMethodUpdate);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
addNotification({
|
||||
@@ -63,10 +63,10 @@
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
dismissible={false}
|
||||
bind:error
|
||||
onSubmit={handleSubmit}
|
||||
bind:show
|
||||
dismissible={false}
|
||||
onSubmit={handleSubmit}
|
||||
title="Update payment method state">
|
||||
<Layout.Stack direction="column" gap="m">
|
||||
<Typography.Text>
|
||||
|
||||
@@ -2,22 +2,18 @@
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { abbreviateNumber, formatCurrency, isWithinSafeRange } from '$lib/helpers/numbers';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Table, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { BillingPlanGroup, type Models } from '@appwrite.io/console';
|
||||
import { abbreviateNumber, formatCurrency, isWithinSafeRange } from '$lib/helpers/numbers';
|
||||
|
||||
export let show = false;
|
||||
export let org: Organization;
|
||||
|
||||
$: plan = $plansInfo?.get(org.billingPlan);
|
||||
export let org: Models.Organization;
|
||||
|
||||
$: nextDate = org?.name
|
||||
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
|
||||
: org?.billingNextInvoiceDate;
|
||||
|
||||
$: isFree = org.billingPlan === BillingPlan.FREE;
|
||||
$: isFree = org.billingPlanDetails.group === BillingPlanGroup.Starter;
|
||||
|
||||
// equal or above means unlimited!
|
||||
const getCorrectSeatsCountValue = (count: number): string | number => {
|
||||
@@ -27,22 +23,22 @@
|
||||
};
|
||||
|
||||
function getPlanLimit(key: string): number | false {
|
||||
return plan[key] || false;
|
||||
return org.billingPlanDetails[key] || false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show title="Usage rates">
|
||||
{#if isFree}
|
||||
<Typography.Text>
|
||||
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
|
||||
Next billing period: {toLocaleDate(nextDate)}.
|
||||
Usage on the {org.billingPlanDetails.name} plan is limited for the following resources. Next
|
||||
billing period: {toLocaleDate(nextDate)}.
|
||||
</Typography.Text>
|
||||
{:else if org.billingPlan === BillingPlan.PRO}
|
||||
{:else if org.billingPlanDetails.group === BillingPlanGroup.Pro}
|
||||
<Typography.Text>
|
||||
Usage on the Pro plan will be charged at the end of each billing period at the following
|
||||
rates. Next billing period: {toLocaleDate(nextDate)}.
|
||||
</Typography.Text>
|
||||
{:else if org.billingPlan === BillingPlan.SCALE}
|
||||
{:else if org.billingPlanDetails.group === BillingPlanGroup.Scale}
|
||||
<Typography.Text>
|
||||
Usage on the Scale plan will be charged at the end of each billing period at the
|
||||
following rates. Next billing period: {toLocaleDate(nextDate)}.
|
||||
@@ -56,7 +52,7 @@
|
||||
<Table.Header.Cell column="limit" {root}>Limit</Table.Header.Cell>
|
||||
<Table.Header.Cell column="rate" {root}>Rate</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each Object.values(plan.addons) as addon}
|
||||
{#each Object.values(org.billingPlanDetails.addons) as addon}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="resource" {root}>{addon.invoiceDesc}</Table.Cell>
|
||||
<Table.Cell column="limit" {root}>
|
||||
@@ -69,7 +65,7 @@
|
||||
{/if}
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
{#each Object.entries(plan.usage) as [key, usage]}
|
||||
{#each Object.entries(org.billingPlanDetails.usage) as [key, usage]}
|
||||
{@const limit = getPlanLimit(key)}
|
||||
{@const show = limit !== false}
|
||||
{#if show}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let isNewOrg = false;
|
||||
export let couponData: Partial<Coupon> = {
|
||||
export let couponData: Partial<Models.Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
|
||||
let error: string = null;
|
||||
let coupon: string = '';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
async function addCoupon() {
|
||||
try {
|
||||
// const response = await sdk.forConsole.billing.getCoupon(coupon);
|
||||
const response = await sdk.forConsole.billing.getCouponAccount(coupon); //TODO: double check that this is the correct method
|
||||
const response = await sdk.forConsole.account.getCoupon({
|
||||
couponId: coupon
|
||||
});
|
||||
|
||||
if (response.onlyNewOrgs && !isNewOrg) {
|
||||
show = false;
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
} from '$lib/stores/bottom-alerts';
|
||||
import { onMount } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { canUpgrade, getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
|
||||
import { project } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { page } from '$app/state';
|
||||
@@ -142,13 +141,14 @@
|
||||
// the button component cannot have both href and on:click!
|
||||
function triggerWindowLink(alert: BottomModalAlertItem, event?: string) {
|
||||
const alertAction = alert.cta;
|
||||
const shouldShowUpgrade = showUpgrade();
|
||||
const shouldShowUpgrade = canUpgrade($organization?.billingPlanDetails);
|
||||
|
||||
// for correct event tracking after removal
|
||||
const currentModalId = currentModalAlert.id;
|
||||
const organizationId = $project.teamId ?? $organization.$id;
|
||||
|
||||
const url = shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
? getChangePlanUrl(organizationId)
|
||||
: alertAction.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
@@ -170,26 +170,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
function showUpgrade() {
|
||||
const plan = currentModalAlert.plan;
|
||||
const organizationPlan = $organization?.billingPlan;
|
||||
switch (plan) {
|
||||
case 'free':
|
||||
return false;
|
||||
case 'pro':
|
||||
return organizationPlan === BillingPlan.FREE;
|
||||
case 'scale':
|
||||
return (
|
||||
organizationPlan === BillingPlan.FREE || organizationPlan === BillingPlan.PRO
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(addBottomModalAlerts);
|
||||
</script>
|
||||
|
||||
{#if !isOnOnboarding && filteredModalAlerts.length > 0 && currentModalAlert}
|
||||
{@const shouldShowUpgrade = showUpgrade()}
|
||||
{@const shouldShowUpgrade = canUpgrade($organization?.billingPlanDetails)}
|
||||
<div class="main-alert-wrapper is-not-mobile">
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
|
||||
@@ -22,10 +22,9 @@
|
||||
import { base } from '$app/paths';
|
||||
import { currentPlan, newOrgModal, organization } from '$lib/stores/organization';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { ID, type Models, Query } from '@appwrite.io/console';
|
||||
import { BillingPlanGroup, ID, type Models, Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { page } from '$app/state';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
type Organization = {
|
||||
@@ -254,7 +253,9 @@
|
||||
|
||||
let badgeType: 'success' | undefined;
|
||||
$: badgeType =
|
||||
$organization && $organization.billingPlan !== BillingPlan.FREE ? 'success' : undefined;
|
||||
$organization && $organization?.billingPlanDetails?.group !== BillingPlanGroup.Starter
|
||||
? 'success'
|
||||
: undefined;
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={onResize} />
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { getServiceLimit, type PlanServices } from '$lib/stores/billing';
|
||||
|
||||
export let disableEmpty = true;
|
||||
export let offset = 0;
|
||||
export let total = 0;
|
||||
export let event: string = null;
|
||||
export let offset = 0;
|
||||
export let service = '';
|
||||
export let disableEmpty = true;
|
||||
export let event: string = null;
|
||||
export let serviceId: PlanServices = service as PlanServices;
|
||||
|
||||
$: planLimit = getServiceLimit(serviceId) || Infinity;
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { project } from '$routes/(console)/project-[region]-[project]/store';
|
||||
|
||||
export let service: string;
|
||||
const {
|
||||
service
|
||||
}: {
|
||||
service: string;
|
||||
} = $props();
|
||||
|
||||
const organizationId: string | null = $derived.by(() => {
|
||||
return $project.teamId ?? $organization.$id;
|
||||
});
|
||||
</script>
|
||||
|
||||
<article class="card u-grid u-cross-center u-width-full-line">
|
||||
@@ -11,7 +21,7 @@
|
||||
<p class="text u-text-center">Upgrade your plan to add more {service}</p>
|
||||
<Button
|
||||
secondary
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl(organizationId)}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, { source: 'card_plan_limit' });
|
||||
}}>Change plan</Button>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { Badge, Layout, Link, Popover, Table } from '@appwrite.io/pink-svelte';
|
||||
import CreditCardBrandImage from './creditCardBrandImage.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { TableRootProp } from '$lib/helpers/types';
|
||||
import CreditCardBrandImage from './creditCardBrandImage.svelte';
|
||||
import { Badge, Layout, Link, Popover, Table } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let root: TableRootProp;
|
||||
export let paymentMethod: PaymentMethodData;
|
||||
export let isBackup: boolean = false;
|
||||
export let paymentMethod: Models.PaymentMethod;
|
||||
</script>
|
||||
|
||||
<Table.Cell column="cc" {root}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { getNextTier, tierToPlan } from '$lib/stores/billing';
|
||||
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { getNextTierBillingPlan } from '$lib/stores/billing';
|
||||
|
||||
export let source = 'empty_state_card';
|
||||
export let responsive = false;
|
||||
@@ -11,6 +11,8 @@
|
||||
let direction: 'column' | 'row' | 'row-reverse' | 'column-reverse' = 'row';
|
||||
|
||||
$: direction = responsive ? ($isSmallViewport ? 'column' : 'row') : 'row';
|
||||
|
||||
$: nextTierBillingPlan = getNextTierBillingPlan($organization.billingPlanId);
|
||||
</script>
|
||||
|
||||
<Card.Base variant="secondary" padding="s" radius="s">
|
||||
@@ -24,7 +26,7 @@
|
||||
<Layout.Stack gap="xxs">
|
||||
<Typography.Text variant="m-600"><slot name="title" /></Typography.Text>
|
||||
<Typography.Text>
|
||||
<slot nextTier={tierToPlan(getNextTier($organization.billingPlan)).name} />
|
||||
<slot nextTier={nextTierBillingPlan?.name} />
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<slot name="cta" {source} />
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
page.url.href,
|
||||
$user.name,
|
||||
$user.email,
|
||||
$organization?.billingPlan,
|
||||
$organization?.billingPlanId,
|
||||
$feedbackData.value,
|
||||
$organization?.$id,
|
||||
$project?.$id,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { getServiceLimit, plansInfo } from '$lib/stores/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { getBasePlanFromGroup, getServiceLimit } from '$lib/stores/billing';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { Badge, Icon, Layout, Table, Typography, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconArrowUp, IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
@@ -11,15 +10,15 @@
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { organization, type Organization } from '$lib/stores/organization';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlanGroup, type Models } from '@appwrite.io/console';
|
||||
|
||||
// Props
|
||||
type Props = {
|
||||
organization: Organization;
|
||||
storageUsage?: number;
|
||||
projects?: Models.Project[];
|
||||
members?: Models.Membership[];
|
||||
storageUsage?: number;
|
||||
organization: Models.Organization;
|
||||
};
|
||||
|
||||
const { projects = [], members = [], storageUsage = 0 }: Props = $props();
|
||||
@@ -29,11 +28,13 @@
|
||||
let error = $state<string | null>(null);
|
||||
let showSelectionReminder = $state(false);
|
||||
|
||||
const baseFreePlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
|
||||
// Derived state using runes
|
||||
let freePlanLimits = $derived({
|
||||
projects: $plansInfo?.get(BillingPlan.FREE)?.projects,
|
||||
members: getServiceLimit('members', BillingPlan.FREE),
|
||||
storage: getServiceLimit('storage', BillingPlan.FREE)
|
||||
projects: baseFreePlan?.projects,
|
||||
members: getServiceLimit('members', null, baseFreePlan),
|
||||
storage: getServiceLimit('storage', null, baseFreePlan)
|
||||
});
|
||||
|
||||
// When preparing to downgrade to Free, enforce Free plan limit locally (2)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script>
|
||||
import Base from './base.svelte';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { Badge, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
|
||||
</script>
|
||||
@@ -11,7 +10,7 @@
|
||||
<Base>
|
||||
<Layout.Stack gap="s">
|
||||
{#if isCloud}
|
||||
{#if $organization?.billingPlan !== BillingPlan.FREE}
|
||||
{#if $organization?.billingPlanDetails.supportsOrganizationRoles}
|
||||
<Typography.Text variant="m-600">Roles</Typography.Text>
|
||||
<Typography.Text>Owner, Developer, Editor, Analyst and Billing.</Typography.Text>
|
||||
<Typography.Text>
|
||||
@@ -39,7 +38,8 @@
|
||||
text
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</Button>
|
||||
<Button size="s" secondary external href={$upgradeURL}>Upgrade plan</Button>
|
||||
<Button size="s" secondary external href={getChangePlanUrl($organization?.$id)}
|
||||
>Upgrade plan</Button>
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -59,7 +59,8 @@
|
||||
text
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</Button>
|
||||
<Button size="s" secondary external href={$upgradeURL}>Upgrade to Cloud</Button>
|
||||
<Button size="s" secondary external href={getChangePlanUrl($organization?.$id)}
|
||||
>Upgrade to Cloud</Button>
|
||||
</p>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
import { isSupportOnline, showSupportModal } from '$routes/(console)/wizard/support/store';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { localeShortTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { Card } from '$lib/components/index';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { currentPlan, type Organization, organizationList } from '$lib/stores/organization';
|
||||
import { currentPlan, organizationList } from '$lib/stores/organization';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
import { base } from '$app/paths';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
$: hasPremiumSupport = $currentPlan?.premiumSupport ?? allOrgsHavePremiumSupport ?? false;
|
||||
|
||||
$: allOrgsHavePremiumSupport = $organizationList.teams.every(
|
||||
(team) => $plansInfo.get((team as Organization).billingPlan)?.premiumSupport
|
||||
(team) => (team as Models.Organization).billingPlanDetails.premiumSupport
|
||||
);
|
||||
|
||||
// there can only be one free organization
|
||||
$: freeOrganization = $organizationList.teams.find(
|
||||
(team) => !$plansInfo.get((team as Organization).billingPlan)?.premiumSupport
|
||||
(team) => !(team as Models.Organization).billingPlanDetails.premiumSupport
|
||||
);
|
||||
|
||||
$: upgradeURL = `${base}/organization-${freeOrganization?.$id}/change-plan`;
|
||||
|
||||
@@ -609,17 +609,6 @@ export const eventServices: Array<EventService> = [
|
||||
}
|
||||
];
|
||||
|
||||
export enum BillingPlan {
|
||||
FREE = 'tier-0',
|
||||
PRO = 'tier-1',
|
||||
SCALE = 'tier-2',
|
||||
GITHUB_EDUCATION = 'auto-1',
|
||||
CUSTOM = 'cont-1',
|
||||
ENTERPRISE = 'ent-1'
|
||||
}
|
||||
|
||||
export const BASE_BILLING_PLANS: string[] = [BillingPlan.FREE, BillingPlan.PRO, BillingPlan.SCALE];
|
||||
|
||||
export const feedbackDowngradeOptions = [
|
||||
{
|
||||
value: 'availableFeatures',
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { getServiceLimit, upgradeURL, type PlanServices } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Button } from '../forms';
|
||||
|
||||
let tableBody: HTMLDivElement;
|
||||
|
||||
export let service: PlanServices = null;
|
||||
export let name = service;
|
||||
export let total: number = null;
|
||||
export let event: string = null;
|
||||
|
||||
let columns = 0;
|
||||
|
||||
const limit = getServiceLimit(service) || Infinity;
|
||||
|
||||
// TODO: refactor this to be a string
|
||||
const upgradeMethod = () => {
|
||||
goto($upgradeURL);
|
||||
};
|
||||
|
||||
$: limitReached = limit !== 0 && limit < Infinity && total >= limit;
|
||||
|
||||
$: if (tableBody) {
|
||||
columns = tableBody?.parentNode?.querySelectorAll('.table-thead-col')?.length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-tbody" role="rowgroup" bind:this={tableBody}>
|
||||
<slot />
|
||||
</div>
|
||||
{#if isCloud && limitReached && service}
|
||||
<tr class="table-row">
|
||||
<td class="table-col" width="100%" colspan={columns}>
|
||||
<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
|
||||
href={$upgradeURL}
|
||||
on:click={() =>
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
source: event ?? 'table_row_limit_reached'
|
||||
})}>Upgrade plan</Button>
|
||||
</slot>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
@@ -1,24 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let title = '';
|
||||
export let onlyDesktop = false;
|
||||
export let width: number = null;
|
||||
export let showOverflow = false;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
export let style = '';
|
||||
export let right = false;
|
||||
</script>
|
||||
|
||||
<div
|
||||
{style}
|
||||
style:--p-col-width={width?.toString() ?? ''}
|
||||
class="table-col {className}"
|
||||
class:u-overflow-visible={showOverflow}
|
||||
class:is-only-desktop={onlyDesktop}
|
||||
class:u-flex={right}
|
||||
class:u-main-end={right}
|
||||
data-title={title}
|
||||
role="cell"
|
||||
data-private>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Cell from './cell.svelte';
|
||||
export let src: string;
|
||||
export let alt: string;
|
||||
export let onlyDesktop = false;
|
||||
export let width = 30;
|
||||
</script>
|
||||
|
||||
<Cell {onlyDesktop} {width}>
|
||||
<div class="image">
|
||||
<img class="avatar" {width} height={width} {src} {alt} />
|
||||
</div>
|
||||
</Cell>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let title = '';
|
||||
export let onlyDesktop = false;
|
||||
export let width: number = null;
|
||||
export let showOverflow = false;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
export let style = '';
|
||||
export let right = false;
|
||||
</script>
|
||||
|
||||
<button
|
||||
{style}
|
||||
style:--p-col-width={width?.toString() ?? ''}
|
||||
class="table-col {className}"
|
||||
class:u-overflow-visible={showOverflow}
|
||||
class:is-only-desktop={onlyDesktop}
|
||||
class:u-flex={right}
|
||||
class:u-main-end={right}
|
||||
data-title={title}
|
||||
role="cell"
|
||||
on:click
|
||||
on:keypress
|
||||
data-private>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { toggle } from '$lib/helpers/array';
|
||||
import { isHTMLInputElement } from '$lib/helpers/types';
|
||||
import { TableCellButton } from '.';
|
||||
import { InputCheckbox } from '../forms';
|
||||
|
||||
export let id: string;
|
||||
export let selectedIds: string[] = [];
|
||||
export let disabled: boolean = false;
|
||||
let el: HTMLInputElement;
|
||||
|
||||
const handleClick = (e: Event) => {
|
||||
// Prevent the link from being followed
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isHTMLInputElement(el)) return;
|
||||
|
||||
selectedIds = toggle(selectedIds, id);
|
||||
|
||||
// Hack to make sure the checkbox is checked, independent of the
|
||||
// preventDefault() call above
|
||||
window.setTimeout(() => {
|
||||
el.checked = selectedIds.includes(id);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<TableCellButton on:click={handleClick}>
|
||||
<InputCheckbox
|
||||
bind:element={el}
|
||||
id="select-{id}"
|
||||
checked={selectedIds.includes(id)}
|
||||
{disabled}
|
||||
on:click={handleClick} />
|
||||
</TableCellButton>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let width: number = null;
|
||||
export let onlyDesktop = false;
|
||||
export let eyebrow = true;
|
||||
export let style = '';
|
||||
</script>
|
||||
|
||||
<div
|
||||
style:--p-col-width={width?.toString()}
|
||||
class:is-only-desktop={onlyDesktop}
|
||||
{style}
|
||||
class="table-thead-col"
|
||||
role="columnheader">
|
||||
<span class={eyebrow ? 'eyebrow-heading-3' : 'body-text-2 u-bold'}>
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { isHTMLInputElement } from '$lib/helpers/types';
|
||||
import { TableCellHead } from '.';
|
||||
import { InputCheckbox } from '../forms';
|
||||
|
||||
export let selected: string[] = [];
|
||||
export let pageItemsIds: string[] = [];
|
||||
|
||||
function handleClick(e: CustomEvent) {
|
||||
if (!isHTMLInputElement(e.target)) return;
|
||||
if (e.target.checked) {
|
||||
const set = new Set(selected);
|
||||
pageItemsIds.forEach((id) => set.add(id));
|
||||
selected = Array.from(set);
|
||||
} else {
|
||||
selected = selected.filter((id) => {
|
||||
return !pageItemsIds.includes(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: someSelected = pageItemsIds.some((id) => selected.includes(id));
|
||||
$: allSelected = pageItemsIds.every((id) => selected.includes(id));
|
||||
</script>
|
||||
|
||||
<TableCellHead width={10}>
|
||||
<InputCheckbox
|
||||
id="select-all"
|
||||
indeterminate={someSelected && !allSelected}
|
||||
checked={allSelected}
|
||||
on:click={handleClick} />
|
||||
</TableCellHead>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
export let title: string;
|
||||
|
||||
export let external = false;
|
||||
export let noStyle = false;
|
||||
</script>
|
||||
|
||||
<div class="table-col" data-title={title} role="cell" data-private>
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class:link={!noStyle}
|
||||
{href}
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}><slot /></a>
|
||||
</div>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Trim } from '$lib/components';
|
||||
import Cell from './cell.svelte';
|
||||
export let title = '';
|
||||
export let showOverflow = false;
|
||||
export let onlyDesktop = false;
|
||||
export let width: number = null;
|
||||
export let right = false;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
export let style = '';
|
||||
</script>
|
||||
|
||||
<Cell {title} {showOverflow} {onlyDesktop} {width} {right} class={className} {style}>
|
||||
<Trim><slot /></Trim>
|
||||
</Cell>
|
||||
@@ -1,5 +0,0 @@
|
||||
<tfoot>
|
||||
<tr>
|
||||
<slot />
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -1,5 +0,0 @@
|
||||
<div class="table-thead" role="rowheader">
|
||||
<div class="table-row" role="row">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,17 +1 @@
|
||||
export { default as Table } from './table.svelte';
|
||||
export { default as TableScroll } from './tableScroll.svelte';
|
||||
export { default as TableList } from './tableList.svelte';
|
||||
export { default as TableBody } from './body.svelte';
|
||||
export { default as TableHeader } from './header.svelte';
|
||||
export { default as TableFooter } from './footer.svelte';
|
||||
export { default as TableRow } from './row.svelte';
|
||||
export { default as TableRowLink } from './rowLink.svelte';
|
||||
export { default as TableRowButton } from './rowButton.svelte';
|
||||
export { default as TableCell } from './cell.svelte';
|
||||
export { default as TableCellButton } from './cellButton.svelte';
|
||||
export { default as TableCellHead } from './cellHead.svelte';
|
||||
export { default as TableCellHeadCheck } from './cellHeadCheck.svelte';
|
||||
export { default as TableCellLink } from './cellLink.svelte';
|
||||
export { default as TableCellAvatar } from './cellAvatar.svelte';
|
||||
export { default as TableCellText } from './cellText.svelte';
|
||||
export { default as TableCellCheck } from './cellCheck.svelte';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="table-row" role="row">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
</script>
|
||||
|
||||
<tr class="table-row" role="button" tabindex="0" on:keyup={clickOnEnter} on:click|preventDefault>
|
||||
<slot />
|
||||
</tr>
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
</script>
|
||||
|
||||
<a class="table-row" role="row" {href}>
|
||||
<slot />
|
||||
</a>
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let noMargin = false;
|
||||
export let noStyles = false;
|
||||
export let style = '';
|
||||
export let transparent = false;
|
||||
export let isAutoLayout = false;
|
||||
export let tag: 'div' | 'table' = 'div';
|
||||
export let dense = false;
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={tag}
|
||||
class="table is-selected-columns-mobile"
|
||||
class:is-table-layout-auto={isAutoLayout}
|
||||
class:u-margin-block-start-32={!noMargin}
|
||||
class:is-remove-outer-styles={noStyles}
|
||||
class:is-table-row-medium-size={dense}
|
||||
{style}
|
||||
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}
|
||||
role="table"
|
||||
data-private>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
<style>
|
||||
:global(.table .button.is-text) {
|
||||
--p-text-color-default: initial !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +0,0 @@
|
||||
<div class="table-with-scroll" data-private>
|
||||
<div class="table-wrapper">
|
||||
<div class="table is-remove-outer-styles">
|
||||
<ul class="table-thead">
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
let isOverflowing = false;
|
||||
|
||||
const hasOverflow: Action<HTMLDivElement, unknown> = (node) => {
|
||||
const hasOverflow: Action<HTMLDivElement, unknown> = (node: Element) => {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
let overflowing = false;
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { env } from '$env/dynamic/public';
|
||||
import type { Account } from './stores/user';
|
||||
import type { Organization } from './stores/organization';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
// Parse feature flags from env as a string array (exact match only)
|
||||
const flagsRaw = (env.PUBLIC_CONSOLE_FEATURE_FLAGS ?? '').split(',');
|
||||
@@ -8,7 +8,7 @@ const flagsRaw = (env.PUBLIC_CONSOLE_FEATURE_FLAGS ?? '').split(',');
|
||||
// @ts-expect-error: unused method!
|
||||
function isFlagEnabled(name: string) {
|
||||
// loose generic to allow safe access while retaining type safety
|
||||
return <T extends { account?: Account; organization?: Organization }>(data: T) => {
|
||||
return <T extends { account?: Account; organization?: Models.Organization }>(data: T) => {
|
||||
const { account, organization } = data;
|
||||
|
||||
return !!(
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export function makePlansMap(
|
||||
plansArray: Models.BillingPlanList | null
|
||||
): Map<string, Models.BillingPlan> {
|
||||
const plansMap = new Map<string, Models.BillingPlan>();
|
||||
if (!plansArray?.plans.length) return plansMap;
|
||||
|
||||
const plans = plansArray.plans;
|
||||
for (let index = 0; index < plans.length; index++) {
|
||||
const plan = plans[index];
|
||||
plansMap.set(plan.$id, plan);
|
||||
}
|
||||
|
||||
return plansMap;
|
||||
}
|
||||
@@ -6,12 +6,15 @@ import type { DatabaseType, Field } from '$database/(entity)';
|
||||
import { coerceToNumber, isWithinSafeRange } from '$lib/helpers/numbers';
|
||||
|
||||
export async function generateFields(
|
||||
project: Models.Project,
|
||||
project: {
|
||||
id: string;
|
||||
region: string;
|
||||
},
|
||||
databaseId: string,
|
||||
tableId: string,
|
||||
databaseType: DatabaseType
|
||||
): Promise<Field[]> {
|
||||
const client = sdk.forProject(project.region, project.$id);
|
||||
const client = sdk.forProject(project.region, project.id);
|
||||
|
||||
switch (databaseType) {
|
||||
case 'legacy':
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
/**
|
||||
* Models.BillingPlan > program
|
||||
* |_ Models.Program > icon
|
||||
* |_ icon > string
|
||||
*
|
||||
* So we need to map them as needed from Pink icons library
|
||||
*/
|
||||
export const IconsMap = {
|
||||
github: IconGithub
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { BillingPlanGroup } from '@appwrite.io/console';
|
||||
|
||||
export let title: string;
|
||||
export let tooltipContent =
|
||||
$organization?.billingPlan === BillingPlan.FREE
|
||||
$organization?.billingPlanDetails.group === BillingPlanGroup.Starter
|
||||
? `Upgrade to add more ${title.toLocaleLowerCase()}`
|
||||
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
|
||||
tierToPlan($organization?.billingPlan)?.name
|
||||
$organization?.billingPlanDetails.name
|
||||
} plan`;
|
||||
|
||||
export let disabled: boolean;
|
||||
export let buttonText: string;
|
||||
export let buttonMethod: () => void | Promise<void> = () => {};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { DropList } from '$lib/components';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Link } from '$lib/elements';
|
||||
import { Badge, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
@@ -11,11 +10,11 @@
|
||||
getServiceLimit,
|
||||
readOnly,
|
||||
showUsageRatesModal,
|
||||
tierToPlan,
|
||||
upgradeURL,
|
||||
type PlanServices
|
||||
getChangePlanUrl,
|
||||
type PlanServices,
|
||||
canUpgrade
|
||||
} from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { ContainerButton } from '.';
|
||||
@@ -38,18 +37,15 @@
|
||||
let showDropdown = false;
|
||||
|
||||
// TODO: remove the default billing limits when backend is updated with billing code
|
||||
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {
|
||||
const { bandwidth, storage, users, executions } = $organization?.billingLimits ?? {
|
||||
bandwidth: 1,
|
||||
documents: 1,
|
||||
storage: 1,
|
||||
users: 1,
|
||||
executions: 1
|
||||
};
|
||||
|
||||
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
|
||||
const limitedServices = [
|
||||
{ name: 'bandwidth', value: bandwidth },
|
||||
{ name: 'documents', value: documents },
|
||||
{ name: 'storage', value: storage },
|
||||
{ name: 'users', value: users },
|
||||
{ name: 'executions', value: executions }
|
||||
@@ -60,18 +56,20 @@
|
||||
//TODO: refactor this to be a string
|
||||
const upgradeMethod = () => {
|
||||
showDropdown = false;
|
||||
goto($upgradeURL);
|
||||
goto(getChangePlanUrl($organization?.$id));
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: tier = tierToPlan($organization?.billingPlan)?.name;
|
||||
$: planName = $organization?.billingPlanDetails.name;
|
||||
// these can be organization level limitations as well.
|
||||
// we need to migrate this sometime later, but soon!
|
||||
$: hasProjectLimitation =
|
||||
checkForProjectLimitation(serviceId) && $organization?.billingPlan === BillingPlan.FREE;
|
||||
$: hasProjectLimitation = checkForProjectLimitation($organization?.billingPlanId, serviceId);
|
||||
|
||||
$: hasUsageFees = hasProjectLimitation
|
||||
? checkForUsageFees($organization?.billingPlan, serviceId)
|
||||
? checkForUsageFees($organization?.billingPlanId, serviceId)
|
||||
: false;
|
||||
|
||||
$: isLimited = limit !== 0 && limit < Infinity;
|
||||
$: overflowingServices = limitedServices.filter((service) => service.value > 0);
|
||||
$: isButtonDisabled =
|
||||
@@ -80,12 +78,12 @@
|
||||
(isLimited && total >= limit && !hasUsageFees);
|
||||
|
||||
onMount(() => {
|
||||
dispatch('data', { isButtonDisabled, limit, tier });
|
||||
dispatch('data', { isButtonDisabled, limit, tier: planName });
|
||||
});
|
||||
|
||||
// on free plan, if the only db is deleted,
|
||||
// `create database` button needs to be enabled again.
|
||||
$: if (isLimited) dispatch('data', { isButtonDisabled, limit, tier });
|
||||
$: if (isLimited) dispatch('data', { isButtonDisabled, limit, tier: planName });
|
||||
</script>
|
||||
|
||||
<!-- Show only if on Cloud, alerts are enabled, and it isn't a project limited service -->
|
||||
@@ -98,11 +96,19 @@
|
||||
.join(', ')}
|
||||
|
||||
{#if services.length}
|
||||
<slot name="alert" {limit} {tier} {title} {upgradeMethod} {hasUsageFees} {services}>
|
||||
{#if $organization?.billingPlan !== BillingPlan.FREE && hasUsageFees}
|
||||
{@const supportsUsage = Object.keys($currentPlan.usage).length > 0}
|
||||
<slot
|
||||
name="alert"
|
||||
{limit}
|
||||
tier={planName}
|
||||
{title}
|
||||
{upgradeMethod}
|
||||
{hasUsageFees}
|
||||
{services}>
|
||||
{#if !supportsUsage && hasUsageFees}
|
||||
<Alert.Inline status="info">
|
||||
<span class="text">
|
||||
You've reached the {services} limit for the {tier} plan.
|
||||
You've reached the {services} limit for the {planName} plan.
|
||||
<Link on:mousedown={() => ($showUsageRatesModal = true)}
|
||||
>Excess usage fees will apply</Link
|
||||
>.
|
||||
@@ -111,8 +117,8 @@
|
||||
{:else}
|
||||
<Alert.Inline status={alertType}>
|
||||
<span class="text">
|
||||
You've reached the {services} limit for the {tier} plan. <Link
|
||||
href={$upgradeURL}
|
||||
You've reached the {services} limit for the {planName} plan. <Link
|
||||
href={getChangePlanUrl($organization?.$id)}
|
||||
event="organization_upgrade"
|
||||
eventData={{ from: 'event', source: 'inline_alert' }}>Upgrade</Link> your
|
||||
organization for additional resources.
|
||||
@@ -135,7 +141,7 @@
|
||||
on:click={() => (showDropdown = !showDropdown)}>
|
||||
<Icon icon={IconInfo} size="s" slot="start" />
|
||||
</Badge>
|
||||
{:else if $organization?.billingPlan !== BillingPlan.SCALE}
|
||||
{:else}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
content="Limits applied"
|
||||
@@ -144,13 +150,19 @@
|
||||
</Badge>
|
||||
{/if}
|
||||
<svelte:fragment slot="list">
|
||||
<slot name="tooltip" {limit} {tier} {title} {upgradeMethod} {hasUsageFees}>
|
||||
<slot
|
||||
name="tooltip"
|
||||
{limit}
|
||||
tier={planName}
|
||||
{title}
|
||||
{upgradeMethod}
|
||||
{hasUsageFees}>
|
||||
{#if hasProjectLimitation}
|
||||
<p class="text">
|
||||
You are limited to {limit}
|
||||
{title.toLocaleLowerCase()} per project on the {tier} plan.
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}<Link
|
||||
href={$upgradeURL}
|
||||
{title.toLocaleLowerCase()} per project on the {planName} plan.
|
||||
{#if canUpgrade($organization.billingPlanId)}<Link
|
||||
href={getChangePlanUrl($organization?.$id)}
|
||||
event="organization_upgrade"
|
||||
eventData={{ from: 'button', source: 'resource_limit_tag' }}
|
||||
>Upgrade</Link>
|
||||
@@ -160,7 +172,7 @@
|
||||
{:else if hasUsageFees}
|
||||
<p class="text">
|
||||
You are limited to {limit}
|
||||
{title.toLocaleLowerCase()} per organization on the {tier} plan.
|
||||
{title.toLocaleLowerCase()} per organization on the {planName} plan.
|
||||
<Link on:mousedown={() => ($showUsageRatesModal = true)}
|
||||
>Excess usage fees will apply</Link
|
||||
>.
|
||||
@@ -168,9 +180,9 @@
|
||||
{:else}
|
||||
<p class="text">
|
||||
You are limited to {limit}
|
||||
{title.toLocaleLowerCase()} per organization on the {tier} plan.
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
<Link href={$upgradeURL}>Upgrade</Link>
|
||||
{title.toLocaleLowerCase()} per organization on the {planName} plan.
|
||||
{#if canUpgrade($organization.billingPlanId)}
|
||||
<Link href={getChangePlanUrl($organization?.$id)}>Upgrade</Link>
|
||||
for additional {title.toLocaleLowerCase()}.
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
import { CustomId } from '$lib/components/index.js';
|
||||
import { getFlagUrl } from '$lib/helpers/flag';
|
||||
import { isCloud } from '$lib/system.js';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { filterRegions } from '$lib/helpers/regions';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
let {
|
||||
projectName = $bindable(),
|
||||
@@ -20,7 +18,7 @@
|
||||
regions = [],
|
||||
region = $bindable(),
|
||||
showTitle = true,
|
||||
billingPlan = undefined,
|
||||
currentPlan = undefined,
|
||||
projects = undefined,
|
||||
submit
|
||||
}: {
|
||||
@@ -29,21 +27,24 @@
|
||||
regions: Array<Models.ConsoleRegion>;
|
||||
region: string;
|
||||
showTitle: boolean;
|
||||
billingPlan?: BillingPlan;
|
||||
currentPlan?: Models.BillingPlan;
|
||||
projects?: number;
|
||||
submit?: Snippet;
|
||||
} = $props();
|
||||
|
||||
let showCustomId = $state(false);
|
||||
let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO);
|
||||
let projectsLimited = $derived(
|
||||
$currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects
|
||||
);
|
||||
let isAddonProject = $derived(
|
||||
$currentPlan?.addons?.projects?.supported &&
|
||||
|
||||
const projectsLimited = $derived.by(() => {
|
||||
return currentPlan?.projects > 0 && projects && projects >= currentPlan?.projects;
|
||||
});
|
||||
|
||||
const isAddonProject = $derived.by(() => {
|
||||
return (
|
||||
currentPlan?.addons?.projects?.supported &&
|
||||
projects &&
|
||||
projects >= $currentPlan?.addons?.projects?.planIncluded
|
||||
);
|
||||
projects >= currentPlan?.addons?.projects?.planIncluded
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -61,7 +62,7 @@
|
||||
<Layout.Stack direction="column" gap="xxl">
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
<Input.Text
|
||||
disabled={!isProPlan && projectsLimited}
|
||||
disabled={projectsLimited}
|
||||
label="Name"
|
||||
placeholder="Project name"
|
||||
required
|
||||
@@ -82,7 +83,7 @@
|
||||
{#if isCloud && regions.length > 0}
|
||||
<Layout.Stack gap="xs">
|
||||
<Input.Select
|
||||
disabled={!isProPlan && projectsLimited}
|
||||
disabled={projectsLimited}
|
||||
required
|
||||
bind:value={region}
|
||||
placeholder="Select a region"
|
||||
@@ -91,25 +92,29 @@
|
||||
<Typography.Text>Region cannot be changed after creation</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
{#if isAddonProject}
|
||||
<Alert.Inline
|
||||
status="info"
|
||||
title="Expand for {formatCurrency(
|
||||
$currentPlan?.addons?.projects?.price || 15
|
||||
currentPlan?.addons?.projects?.price || 15
|
||||
)}/project per month">
|
||||
Each added project comes with its own dedicated pool of resources.
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
{#if projectsLimited}
|
||||
<Alert.Inline
|
||||
status="warning"
|
||||
title={`You've reached your limit of ${$currentPlan?.projects} projects`}>
|
||||
title={`You've reached your limit of ${currentPlan?.projects} projects`}>
|
||||
Extra projects are available on paid plans for an additional fee
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
compact
|
||||
size="s"
|
||||
href={`${base}/organization-${page.params.organization}/billing`}
|
||||
href={resolve('/(console)/organization-[organization]/billing', {
|
||||
organization: page.params.organization
|
||||
})}
|
||||
external>Upgrade</Button>
|
||||
</svelte:fragment>
|
||||
</Alert.Inline>
|
||||
|
||||
+13
-10
@@ -9,14 +9,12 @@
|
||||
import { organization, organizationList } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import SideNavigation from '$lib/layout/navigation.svelte';
|
||||
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
|
||||
import { isSidebarOpen, noWidthTransition } from '$lib/stores/sidebar';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { BillingPlanGroup, type Models } from '@appwrite.io/console';
|
||||
import { getSidebarState, isInDatabasesRoute, updateSidebarState } from '$lib/helpers/sidebar';
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
|
||||
@@ -155,14 +153,19 @@
|
||||
})
|
||||
.toString(),
|
||||
|
||||
organizations: $organizationList.teams.map((org) => {
|
||||
const billingPlan = org['billingPlan'];
|
||||
organizations: $organizationList.teams.map((team) => {
|
||||
let billingPlan: Models.BillingPlan | null = null;
|
||||
|
||||
if (isCloud) {
|
||||
billingPlan = (team as Models.Organization).billingPlanDetails;
|
||||
}
|
||||
|
||||
return {
|
||||
name: org.name,
|
||||
$id: org.$id,
|
||||
showUpgrade: billingPlan === BillingPlan.FREE,
|
||||
tierName: isCloud ? tierToPlan(billingPlan).name : null,
|
||||
isSelected: $organization?.$id === org.$id
|
||||
name: team.name,
|
||||
$id: team.$id,
|
||||
isSelected: $organization?.$id === team.$id,
|
||||
tierName: isCloud ? billingPlan.name : null,
|
||||
showUpgrade: isCloud ? billingPlan.group === BillingPlanGroup.Starter : false
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
@@ -5,23 +5,22 @@
|
||||
import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg';
|
||||
import LoginDark from '$lib/images/login/login-dark-mode.png';
|
||||
import LoginLight from '$lib/images/login/login-light-mode.png';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { app } from '$lib/stores/app';
|
||||
import type { Campaign } from '$lib/stores/campaigns';
|
||||
import { Typography, Layout, Avatar } from '@appwrite.io/pink-svelte';
|
||||
import { getCampaignImageUrl } from '$routes/(public)/card/helpers';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const imgLight = LoginLight;
|
||||
export const imgDark = LoginDark;
|
||||
|
||||
export let campaign: Campaign = null;
|
||||
export let coupon: Coupon = null;
|
||||
export let campaign: Models.Campaign = null;
|
||||
export let coupon: Models.Coupon = null;
|
||||
export let align: 'start' | 'center' | 'end' = 'start';
|
||||
|
||||
$: variation = ((coupon?.campaign ?? campaign) ? campaign?.template : 'default') as
|
||||
| 'default'
|
||||
| Campaign['template'];
|
||||
| Models.Campaign['template'];
|
||||
|
||||
let currentReviewNumber = 0;
|
||||
$: currentReview = campaign?.reviews?.[currentReviewNumber];
|
||||
|
||||
+2
-1427
File diff suppressed because it is too large
Load Diff
+248
-179
@@ -1,48 +1,39 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import LimitReached from '$lib/components/billing/alerts/limitReached.svelte';
|
||||
import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte';
|
||||
import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte';
|
||||
import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte';
|
||||
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
|
||||
|
||||
import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion';
|
||||
import type {
|
||||
AddressesList,
|
||||
AggregationTeam,
|
||||
Invoice,
|
||||
InvoiceList,
|
||||
PaymentList,
|
||||
Plan,
|
||||
PlansMap
|
||||
} from '$lib/sdk/billing';
|
||||
import type { BillingPlansMap } from '$lib/sdk/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { activeHeaderAlert, orgMissingPaymentMethod } from '$routes/(console)/store';
|
||||
import { AppwriteException, Query, Platform } from '@appwrite.io/console';
|
||||
import {
|
||||
AppwriteException,
|
||||
BillingPlanGroup,
|
||||
type Models,
|
||||
Platform,
|
||||
Query
|
||||
} from '@appwrite.io/console';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import { headerAlert } from './headerAlert';
|
||||
import { addNotification, notifications } from './notifications';
|
||||
import {
|
||||
currentPlan,
|
||||
organization,
|
||||
type Organization,
|
||||
type OrganizationError
|
||||
} from './organization';
|
||||
import { currentPlan } from './organization';
|
||||
import { canSeeBilling } from './roles';
|
||||
import { sdk } from './sdk';
|
||||
import { user } from './user';
|
||||
|
||||
import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte';
|
||||
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
|
||||
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
|
||||
import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte';
|
||||
|
||||
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1';
|
||||
|
||||
export const roles = [
|
||||
{
|
||||
label: 'Owner',
|
||||
@@ -69,60 +60,120 @@ export const roles = [
|
||||
export const teamStatusReadonly = 'readonly';
|
||||
export const billingLimitOutstandingInvoice = 'outstanding_invoice';
|
||||
|
||||
export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList);
|
||||
export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList);
|
||||
export const plansInfo = derived(page, ($page) => $page.data.plansInfo as PlansMap);
|
||||
export const daysLeftInTrial = writable<number>(0);
|
||||
export const paymentMethods = derived(
|
||||
page,
|
||||
($page) => $page.data.paymentMethods as Models.PaymentMethodList
|
||||
);
|
||||
export const addressList = derived(
|
||||
page,
|
||||
($page) => $page.data.addressList as Models.BillingAddressList
|
||||
);
|
||||
|
||||
export const readOnly = writable<boolean>(false);
|
||||
export const daysLeftInTrial = writable<number>(0);
|
||||
export const plansInfo = writable<BillingPlansMap>(new Map());
|
||||
|
||||
export const showBudgetAlert = derived(
|
||||
page,
|
||||
($page) => ($page.data.organization?.billingLimits.budgetLimit ?? 0) >= 100
|
||||
);
|
||||
|
||||
function getPlansInfoStore(): BillingPlansMap | null {
|
||||
return get(plansInfo) ?? get(page).data?.plansInfo ?? null;
|
||||
}
|
||||
|
||||
function makeBillingPlan(billingPlanOrId: string | Models.BillingPlan): Models.BillingPlan {
|
||||
return typeof billingPlanOrId === 'string' ? billingIdToPlan(billingPlanOrId) : billingPlanOrId;
|
||||
}
|
||||
|
||||
export function getRoleLabel(role: string) {
|
||||
return roles.find((r) => r.value === role)?.label ?? role;
|
||||
}
|
||||
|
||||
export function tierToPlan(tier: Tier) {
|
||||
switch (tier) {
|
||||
case BillingPlan.FREE:
|
||||
return tierFree;
|
||||
case BillingPlan.PRO:
|
||||
return tierPro;
|
||||
case BillingPlan.SCALE:
|
||||
return tierScale;
|
||||
case BillingPlan.GITHUB_EDUCATION:
|
||||
return tierGitHubEducation;
|
||||
case BillingPlan.CUSTOM:
|
||||
return tierCustom;
|
||||
case BillingPlan.ENTERPRISE:
|
||||
return tierEnterprise;
|
||||
default:
|
||||
return tierCustom;
|
||||
export function isStarterPlan(billingPlanOrId: string | Models.BillingPlan): boolean {
|
||||
const billingPlan = makeBillingPlan(billingPlanOrId);
|
||||
return planHasGroup(billingPlan, BillingPlanGroup.Starter);
|
||||
}
|
||||
|
||||
export function canUpgrade(billingPlanOrId: string | Models.BillingPlan): boolean {
|
||||
const billingPlan = makeBillingPlan(billingPlanOrId);
|
||||
const nextTier = getNextTierBillingPlan(billingPlan.$id);
|
||||
|
||||
// defaults back to PRO, so adjust the check!
|
||||
return billingPlan.$id !== nextTier.$id;
|
||||
}
|
||||
|
||||
export function canDowngrade(billingPlanOrId: string | Models.BillingPlan): boolean {
|
||||
const billingPlan = makeBillingPlan(billingPlanOrId);
|
||||
const nextTier = getPreviousTierBillingPlan(billingPlan.$id);
|
||||
|
||||
// defaults back to Starter, so adjust the check!
|
||||
return billingPlan.$id !== nextTier.$id;
|
||||
}
|
||||
|
||||
export function planHasGroup(
|
||||
billingPlanOrId: string | Models.BillingPlan,
|
||||
group: BillingPlanGroup
|
||||
): boolean {
|
||||
const billingPlan = makeBillingPlan(billingPlanOrId);
|
||||
|
||||
return billingPlan?.group === group;
|
||||
}
|
||||
|
||||
export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan {
|
||||
const plansInfoStore = getPlansInfoStore();
|
||||
|
||||
const proPlans = Array.from(plansInfoStore.values()).filter(
|
||||
(plan) => plan.group === billingPlanGroup
|
||||
);
|
||||
|
||||
return proPlans.sort((a, b) => a.order - b.order)[0];
|
||||
}
|
||||
|
||||
export function billingIdToPlan(billingId: string): Models.BillingPlan | null {
|
||||
const plansInfoStore = getPlansInfoStore();
|
||||
if (plansInfoStore.has(billingId)) {
|
||||
return plansInfoStore.get(billingId);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getNextTier(tier: Tier) {
|
||||
switch (tier) {
|
||||
case BillingPlan.FREE:
|
||||
return BillingPlan.PRO;
|
||||
case BillingPlan.PRO:
|
||||
return BillingPlan.SCALE;
|
||||
default:
|
||||
return BillingPlan.PRO;
|
||||
export function getNextTierBillingPlan(billingPlanId: string): Models.BillingPlan {
|
||||
const currentPlanData = billingIdToPlan(billingPlanId);
|
||||
if (!currentPlanData) {
|
||||
/* should never happen but safety! */
|
||||
return getBasePlanFromGroup(BillingPlanGroup.Pro);
|
||||
}
|
||||
|
||||
const currentOrder = currentPlanData.order;
|
||||
const plans = get(plansInfo);
|
||||
|
||||
for (const [, plan] of plans) {
|
||||
if (plan.order === currentOrder + 1) {
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
return getBasePlanFromGroup(BillingPlanGroup.Pro);
|
||||
}
|
||||
|
||||
export function getPreviousTier(tier: Tier) {
|
||||
switch (tier) {
|
||||
case BillingPlan.PRO:
|
||||
return BillingPlan.FREE;
|
||||
case BillingPlan.SCALE:
|
||||
return BillingPlan.PRO;
|
||||
default:
|
||||
return BillingPlan.FREE;
|
||||
export function getPreviousTierBillingPlan(billingPlanId: string): Models.BillingPlan {
|
||||
const currentPlanData = billingIdToPlan(billingPlanId);
|
||||
if (!currentPlanData) {
|
||||
/* should never happen but safety! */
|
||||
return getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
}
|
||||
const currentOrder = currentPlanData.order;
|
||||
const plans = get(plansInfo);
|
||||
|
||||
for (const [, plan] of plans) {
|
||||
if (plan.order === currentOrder - 1) {
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
return getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
}
|
||||
|
||||
export type PlanServices =
|
||||
@@ -151,7 +202,11 @@ export type PlanServices =
|
||||
| 'authPhone'
|
||||
| 'imageTransformations';
|
||||
|
||||
export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan?: Plan): number {
|
||||
export function getServiceLimit(
|
||||
serviceId: PlanServices,
|
||||
tier: string = null,
|
||||
plan?: Models.BillingPlan
|
||||
): number {
|
||||
if (!isCloud) return 0;
|
||||
if (!serviceId) return 0;
|
||||
|
||||
@@ -177,7 +232,7 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan
|
||||
}
|
||||
|
||||
export const failedInvoice = cachedStore<
|
||||
Invoice,
|
||||
Models.Invoice,
|
||||
{
|
||||
load: (orgId: string) => Promise<void>;
|
||||
}
|
||||
@@ -186,9 +241,10 @@ export const failedInvoice = cachedStore<
|
||||
load: async (orgId) => {
|
||||
if (!isCloud) set(null);
|
||||
if (!get(canSeeBilling)) set(null);
|
||||
const failedInvoices = await sdk.forConsole.billing.listInvoices(orgId, [
|
||||
Query.equal('status', 'failed')
|
||||
]);
|
||||
const failedInvoices = await sdk.forConsole.organizations.listInvoices({
|
||||
organizationId: orgId,
|
||||
queries: [Query.equal('status', 'failed')]
|
||||
});
|
||||
// const failedInvoices = invoices.invoices;
|
||||
if (failedInvoices?.invoices?.length > 0) {
|
||||
const firstFailed = failedInvoices.invoices[0];
|
||||
@@ -203,49 +259,16 @@ export const failedInvoice = cachedStore<
|
||||
};
|
||||
});
|
||||
|
||||
export const actionRequiredInvoices = writable<InvoiceList>(null);
|
||||
|
||||
export type TierData = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const tierFree: TierData = {
|
||||
name: 'Free',
|
||||
description: 'A great fit for passion projects and small applications.'
|
||||
};
|
||||
|
||||
export const tierGitHubEducation: TierData = {
|
||||
name: 'GitHub Education',
|
||||
description: 'For members of GitHub student developers program.'
|
||||
};
|
||||
|
||||
export const tierPro: TierData = {
|
||||
name: 'Pro',
|
||||
description:
|
||||
'For production applications that need powerful functionality and resources to scale.'
|
||||
};
|
||||
export const tierScale: TierData = {
|
||||
name: 'Scale',
|
||||
description:
|
||||
'For teams that handle more complex and large projects and need more control and support.'
|
||||
};
|
||||
|
||||
export const tierCustom: TierData = {
|
||||
name: 'Custom',
|
||||
description: 'Team on a custom contract'
|
||||
};
|
||||
|
||||
export const tierEnterprise: TierData = {
|
||||
name: 'Enterprise',
|
||||
description: 'For enterprises that need more power and premium support.'
|
||||
};
|
||||
export const actionRequiredInvoices = writable<Models.InvoiceList>(null);
|
||||
|
||||
export const showUsageRatesModal = writable<boolean>(false);
|
||||
export const useNewPricingModal = derived(currentPlan, ($plan) => $plan?.usagePerProject === true);
|
||||
|
||||
export function checkForUsageFees(plan: Tier, id: PlanServices) {
|
||||
if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) {
|
||||
export function checkForUsageFees(plan: string, id: PlanServices) {
|
||||
const billingPlan = billingIdToPlan(plan);
|
||||
const supportsUsage = Object.keys(billingPlan.usage).length > 0;
|
||||
|
||||
if (supportsUsage) {
|
||||
switch (id) {
|
||||
case 'bandwidth':
|
||||
case 'storage':
|
||||
@@ -260,11 +283,12 @@ export function checkForUsageFees(plan: Tier, id: PlanServices) {
|
||||
} else return false;
|
||||
}
|
||||
|
||||
export function checkForProjectLimitation(id: PlanServices) {
|
||||
// Members are no longer limited on Pro and Scale plans (unlimited seats)
|
||||
export function checkForProjectLimitation(plan: string, id: PlanServices) {
|
||||
if (id === 'members') {
|
||||
const currentTier = get(organization)?.billingPlan;
|
||||
if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) {
|
||||
const billingPlan = billingIdToPlan(plan);
|
||||
const hasUnlimitedProjects = billingPlan.projects === 0;
|
||||
|
||||
if (hasUnlimitedProjects) {
|
||||
return false; // No project limitation for members on Pro/Scale plans
|
||||
}
|
||||
}
|
||||
@@ -284,15 +308,25 @@ export function checkForProjectLimitation(id: PlanServices) {
|
||||
}
|
||||
}
|
||||
|
||||
export function isServiceLimited(serviceId: PlanServices, plan: Tier, total: number) {
|
||||
export function isServiceLimited(
|
||||
serviceId: PlanServices,
|
||||
organization: Models.Organization,
|
||||
total: number
|
||||
) {
|
||||
// total validity
|
||||
if (!total) return false;
|
||||
|
||||
// org and plan validity!
|
||||
if (!organization) return false;
|
||||
if (!('billingPlanId' in organization)) return false;
|
||||
|
||||
const limit = getServiceLimit(serviceId) || Infinity;
|
||||
const isLimited = limit !== 0 && limit < Infinity;
|
||||
const hasUsageFees = checkForUsageFees(plan, serviceId);
|
||||
const hasUsageFees = checkForUsageFees(organization.billingPlanId, serviceId);
|
||||
return isLimited && total >= limit && !hasUsageFees;
|
||||
}
|
||||
|
||||
export function checkForEnterpriseTrial(org: Organization) {
|
||||
export function checkForEnterpriseTrial(org: Models.Organization) {
|
||||
if (!org || !org.billingNextInvoiceDate) return;
|
||||
if (calculateEnterpriseTrial(org) > 0) {
|
||||
headerAlert.add({
|
||||
@@ -304,7 +338,7 @@ export function checkForEnterpriseTrial(org: Organization) {
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateEnterpriseTrial(org: Organization) {
|
||||
export function calculateEnterpriseTrial(org: Models.Organization) {
|
||||
if (!org || !org.billingNextInvoiceDate) return 0;
|
||||
const endDate = new Date(org.billingNextInvoiceDate);
|
||||
const startDate = new Date(org.billingCurrentInvoiceDate);
|
||||
@@ -319,8 +353,9 @@ export function calculateEnterpriseTrial(org: Organization) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function calculateTrialDay(org: Organization) {
|
||||
if (org?.billingPlan === BillingPlan.FREE) return false;
|
||||
export function calculateTrialDay(org: Models.Organization) {
|
||||
if (!org.billingPlanDetails.trial) return false;
|
||||
|
||||
const endDate = new Date(org?.billingStartDate);
|
||||
const today = new Date();
|
||||
|
||||
@@ -333,20 +368,22 @@ export function calculateTrialDay(org: Organization) {
|
||||
return days;
|
||||
}
|
||||
|
||||
export async function checkForProjectsLimit(org: Organization, orgProjectCount?: number) {
|
||||
export async function checkForProjectsLimit(org: Models.Organization, orgProjectCount?: number) {
|
||||
if (!isCloud) return;
|
||||
if (!org) return;
|
||||
|
||||
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
|
||||
const plan = await sdk.forConsole.organizations.getPlan({
|
||||
organizationId: org.$id
|
||||
});
|
||||
if (!plan) return;
|
||||
|
||||
if (plan.$id !== BillingPlan.FREE) return;
|
||||
if (!org.projects) return;
|
||||
if (org.projects.length > 0) return;
|
||||
|
||||
const projectCount = orgProjectCount;
|
||||
if (projectCount === undefined) return;
|
||||
|
||||
// not unlimited and current exceeds plan limits!
|
||||
if (plan.projects > 0 && projectCount > plan.projects) {
|
||||
headerAlert.add({
|
||||
id: 'projectsLimitReached',
|
||||
@@ -357,8 +394,11 @@ export async function checkForProjectsLimit(org: Organization, orgProjectCount?:
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForUsageLimit(org: Organization) {
|
||||
if (org?.status === teamStatusReadonly && org?.remarks === billingLimitOutstandingInvoice) {
|
||||
export async function checkForUsageLimit(organization: Models.Organization) {
|
||||
if (
|
||||
organization?.status === teamStatusReadonly &&
|
||||
organization?.remarks === billingLimitOutstandingInvoice
|
||||
) {
|
||||
headerAlert.add({
|
||||
id: 'teamReadOnlyFailedInvoices',
|
||||
component: TeamReadonlyAlert,
|
||||
@@ -368,12 +408,14 @@ export async function checkForUsageLimit(org: Organization) {
|
||||
readOnly.set(true);
|
||||
return;
|
||||
}
|
||||
if (!org?.billingLimits && org?.status !== teamStatusReadonly) {
|
||||
|
||||
if (!organization?.billingLimits && organization?.status !== teamStatusReadonly) {
|
||||
readOnly.set(false);
|
||||
return;
|
||||
}
|
||||
if (org?.billingPlan !== BillingPlan.FREE) {
|
||||
const { budgetLimit } = org?.billingLimits ?? {};
|
||||
|
||||
if (organization.billingPlanDetails.budgeting) {
|
||||
const { budgetLimit } = organization?.billingLimits ?? {};
|
||||
|
||||
if (budgetLimit && budgetLimit >= 100) {
|
||||
readOnly.set(false);
|
||||
@@ -389,17 +431,15 @@ export async function checkForUsageLimit(org: Organization) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
|
||||
const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {};
|
||||
const { bandwidth, executions, storage, users } = organization?.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 = org.total;
|
||||
const members = organization.total;
|
||||
const memberLimit = getServiceLimit('members');
|
||||
const membersOverflow = memberLimit === Infinity ? 0 : Math.max(0, members - memberLimit);
|
||||
|
||||
@@ -418,10 +458,16 @@ export async function checkForUsageLimit(org: Organization) {
|
||||
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 ${tierToPlan(BillingPlan.FREE).name} 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 ${tierToPlan(BillingPlan.FREE).name} plan limit. Upgrade to ensure there are no service disruptions.`;
|
||||
|
||||
const threshold = 75;
|
||||
const exceededResources = resources.filter((r) => r.value >= threshold);
|
||||
|
||||
let message = `<b>${organization.name}</b> has reached <b>${threshold}%</b> of its ${exceededResources[0].name} limit. Upgrade to ensure there are no service disruptions.`;
|
||||
|
||||
if (exceededResources.length > 1) {
|
||||
message = `Usage for <b>${organization.name}</b> has reached <b>${threshold}%</b> of its plan limits. Upgrade to ensure there are no service disruptions.`;
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: 'warning',
|
||||
isHtml: true,
|
||||
@@ -431,13 +477,21 @@ export async function checkForUsageLimit(org: Organization) {
|
||||
{
|
||||
name: 'View usage',
|
||||
method: () => {
|
||||
goto(`${base}/organization-${org.$id}/usage`);
|
||||
goto(
|
||||
resolve('/(console)/organization-[organization]/usage', {
|
||||
organization: organization.$id
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Upgrade plan',
|
||||
method: () => {
|
||||
goto(`${base}/organization-${org.$id}/change-plan`);
|
||||
goto(
|
||||
resolve('/(console)/organization-[organization]/change-plan', {
|
||||
organization: organization.$id
|
||||
})
|
||||
);
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
source: 'limit_reached_notification'
|
||||
@@ -451,12 +505,13 @@ export async function checkForUsageLimit(org: Organization) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkPaymentAuthorizationRequired(org: Organization) {
|
||||
if (org.billingPlan === BillingPlan.FREE) return;
|
||||
export async function checkPaymentAuthorizationRequired(org: Models.Organization) {
|
||||
if (!org.billingPlanDetails.requiresPaymentMethod) return;
|
||||
|
||||
const invoices = await sdk.forConsole.billing.listInvoices(org.$id, [
|
||||
Query.equal('status', 'requires_authentication')
|
||||
]);
|
||||
const invoices = await sdk.forConsole.organizations.listInvoices({
|
||||
organizationId: org.$id,
|
||||
queries: [Query.equal('status', 'requires_authentication')]
|
||||
});
|
||||
|
||||
if (invoices?.invoices?.length > 0) {
|
||||
headerAlert.add({
|
||||
@@ -471,12 +526,13 @@ export async function checkPaymentAuthorizationRequired(org: Organization) {
|
||||
actionRequiredInvoices.set(invoices);
|
||||
}
|
||||
|
||||
export async function paymentExpired(org: Organization) {
|
||||
export async function paymentExpired(org: Models.Organization) {
|
||||
if (!org?.paymentMethodId) return;
|
||||
const payment = await sdk.forConsole.billing.getOrganizationPaymentMethod(
|
||||
org.$id,
|
||||
org.paymentMethodId
|
||||
);
|
||||
const payment = await sdk.forConsole.organizations.getPaymentMethod({
|
||||
organizationId: org.$id,
|
||||
paymentMethodId: org.paymentMethodId
|
||||
});
|
||||
|
||||
if (!payment?.expiryYear) return;
|
||||
const sessionStorageNotification = sessionStorage.getItem('expiredPaymentNotification');
|
||||
if (sessionStorageNotification === 'true') return;
|
||||
@@ -500,7 +556,7 @@ export async function paymentExpired(org: Organization) {
|
||||
{
|
||||
name: 'Update payment details',
|
||||
method: () => {
|
||||
goto(`${base}/account/payments`);
|
||||
goto(resolve('/account/payments'));
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -514,7 +570,7 @@ export async function paymentExpired(org: Organization) {
|
||||
{
|
||||
name: 'Update payment details',
|
||||
method: () => {
|
||||
goto(`${base}/account/payments`);
|
||||
goto(resolve('/account/payments'));
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -523,7 +579,7 @@ export async function paymentExpired(org: Organization) {
|
||||
sessionStorage.setItem('expiredPaymentNotification', 'true');
|
||||
}
|
||||
|
||||
export function checkForMarkedForDeletion(org: Organization) {
|
||||
export function checkForMarkedForDeletion(org: Models.Organization) {
|
||||
if (org?.markedForDeletion) {
|
||||
headerAlert.add({
|
||||
id: 'markedForDeletion',
|
||||
@@ -535,12 +591,17 @@ export function checkForMarkedForDeletion(org: Organization) {
|
||||
}
|
||||
|
||||
export async function checkForMissingPaymentMethod() {
|
||||
const orgs = await sdk.forConsole.billing.listOrganization([
|
||||
Query.notEqual('billingPlan', BillingPlan.FREE),
|
||||
Query.isNull('paymentMethodId'),
|
||||
Query.isNull('backupPaymentMethodId'),
|
||||
Query.equal('platform', Platform.Appwrite)
|
||||
]);
|
||||
const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
|
||||
const orgs = await sdk.forConsole.organizations.list({
|
||||
queries: [
|
||||
Query.isNull('paymentMethodId'),
|
||||
Query.isNull('backupPaymentMethodId'),
|
||||
Query.equal('platform', Platform.Appwrite),
|
||||
Query.notEqual('billingPlan', starterPlan.$id)
|
||||
]
|
||||
});
|
||||
|
||||
if (orgs?.total) {
|
||||
orgMissingPaymentMethod.set(orgs.teams[0]);
|
||||
headerAlert.add({
|
||||
@@ -553,9 +614,9 @@ export async function checkForMissingPaymentMethod() {
|
||||
}
|
||||
|
||||
// Display upgrade banner for new users after 1 week for 30 days
|
||||
export async function checkForNewDevUpgradePro(org: Organization) {
|
||||
export async function checkForNewDevUpgradePro(org: Models.Organization) {
|
||||
// browser or plan check.
|
||||
if (!browser || org?.billingPlan !== BillingPlan.FREE) return;
|
||||
if (!browser || !org.billingPlanDetails.supportsCredits) return;
|
||||
|
||||
// already dismissed by user!
|
||||
if (localStorage.getItem('newDevUpgradePro')) return;
|
||||
@@ -569,14 +630,16 @@ export async function checkForNewDevUpgradePro(org: Organization) {
|
||||
const accountCreated = new Date(account.$createdAt).getTime();
|
||||
if (now - accountCreated < 1000 * 60 * 60 * 24 * 7) return;
|
||||
|
||||
const organizations = await sdk.forConsole.billing.listOrganization([
|
||||
Query.notEqual('billingPlan', BillingPlan.FREE)
|
||||
]);
|
||||
const organizations = await sdk.forConsole.organizations.list({
|
||||
queries: [Query.notEqual('billingPlan', getBasePlanFromGroup(BillingPlanGroup.Starter).$id)]
|
||||
});
|
||||
|
||||
if (organizations?.total) return;
|
||||
|
||||
try {
|
||||
await sdk.forConsole.billing.getCouponAccount(NEW_DEV_PRO_UPGRADE_COUPON);
|
||||
await sdk.forConsole.console.getCoupon({
|
||||
couponId: NEW_DEV_PRO_UPGRADE_COUPON
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
// already utilized if error is 409
|
||||
@@ -596,32 +659,38 @@ export async function checkForNewDevUpgradePro(org: Organization) {
|
||||
importance: 1
|
||||
});
|
||||
}
|
||||
export const upgradeURL = derived(
|
||||
page,
|
||||
($page) => `${base}/organization-${$page.data?.organization?.$id}/change-plan`
|
||||
);
|
||||
export const billingURL = derived(
|
||||
page,
|
||||
($page) => `${base}/organization-${$page.data?.organization?.$id}/billing`
|
||||
);
|
||||
|
||||
export const hideBillingHeaderRoutes = [base + '/create-organization', base + '/account'];
|
||||
export function getChangePlanUrl(organizationId?: string | null | undefined): string {
|
||||
let orgId = organizationId || null;
|
||||
|
||||
export function calculateExcess(addon: AggregationTeam, plan: Plan) {
|
||||
return {
|
||||
bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth),
|
||||
storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'),
|
||||
executions: calculateResourceSurplus(addon.usageExecutions, plan.executions, 'GB'),
|
||||
members: addon.additionalMembers
|
||||
};
|
||||
if (!orgId) {
|
||||
try {
|
||||
const pageState = get(page);
|
||||
|
||||
const fromUrl = pageState?.params?.organization?.trim?.() || null;
|
||||
const fromOrgData = pageState?.data?.organization?.$id?.trim?.() || null;
|
||||
const fromProjectData = pageState?.data?.project?.teamId?.trim?.() || null;
|
||||
|
||||
orgId = fromUrl ?? fromProjectData ?? fromOrgData;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
if (orgId) {
|
||||
return resolve('/(console)/organization-[organization]/change-plan', {
|
||||
organization: orgId
|
||||
});
|
||||
} else {
|
||||
/* fallback to not crash anything */
|
||||
return resolve('/');
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateResourceSurplus(total: number, limit: number, limitUnit: Size = null) {
|
||||
if (total === undefined || limit === undefined) return 0;
|
||||
const realLimit = (limitUnit ? sizeToBytes(limit, limitUnit) : limit) || Infinity;
|
||||
return total > realLimit ? total - realLimit : 0;
|
||||
}
|
||||
export const hideBillingHeaderRoutes = [resolve('/account'), resolve('/create-organization')];
|
||||
|
||||
export function isOrganization(org: Organization | OrganizationError): org is Organization {
|
||||
return (org as Organization).$id !== undefined;
|
||||
export function isPaymentAuthenticationRequired(
|
||||
org: Models.Organization | Models.PaymentAuthentication
|
||||
): org is Models.PaymentAuthentication {
|
||||
return 'clientSecret' in org;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Component } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import type { NotificationCoolOffOptions } from '$lib/helpers/notifications';
|
||||
|
||||
export type BottomModalAlertAction = {
|
||||
@@ -10,7 +9,7 @@ export type BottomModalAlertAction = {
|
||||
background?: Record<'light' | 'dark', string> | string;
|
||||
backgroundHover?: Record<'light' | 'dark', string> | string;
|
||||
hideOnClick?: boolean;
|
||||
link: (ctx: { organization: Organization; project: Models.Project }) => string;
|
||||
link: (ctx: { organization: Models.Organization; project: Models.Project }) => string;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { BillingPlan } from '$lib/constants';
|
||||
|
||||
export type Campaign = {
|
||||
$id: string;
|
||||
template: string;
|
||||
title: string;
|
||||
description: string;
|
||||
cta?: string;
|
||||
claimed?: string;
|
||||
unclaimed?: string;
|
||||
reviews?: Review[];
|
||||
image?: {
|
||||
dark: string;
|
||||
light: string;
|
||||
};
|
||||
onlyNewOrgs?: boolean;
|
||||
footer?: boolean;
|
||||
plan?: BillingPlan;
|
||||
};
|
||||
|
||||
export type Review = {
|
||||
name: string;
|
||||
image: string;
|
||||
description: string;
|
||||
review: string;
|
||||
};
|
||||
@@ -1,59 +1,8 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Tier } from './billing';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { type Models, Platform } from '@appwrite.io/console';
|
||||
|
||||
export type OrganizationError = {
|
||||
status: number;
|
||||
message: string;
|
||||
teamId: string;
|
||||
invoiceId: string;
|
||||
clientSecret: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type Organization = Models.Team<Record<string, unknown>> & {
|
||||
billingBudget: number;
|
||||
billingPlan: Tier;
|
||||
billingPlanId: Tier /* unused for now! */;
|
||||
billingPlanDetails: Plan /* unused for now! */;
|
||||
budgetAlerts: number[];
|
||||
paymentMethodId: string;
|
||||
backupPaymentMethodId: string;
|
||||
markedForDeletion: boolean;
|
||||
billingLimits: BillingLimits;
|
||||
billingCurrentInvoiceDate: string;
|
||||
billingNextInvoiceDate: string;
|
||||
billingTrialStartDate?: string;
|
||||
billingStartDate?: string;
|
||||
billingTrialDays?: number;
|
||||
billingAddressId?: string;
|
||||
amount: number;
|
||||
billingTaxId?: string;
|
||||
billingPlanDowngrade?: Tier;
|
||||
billingAggregationId: string;
|
||||
billingInvoiceId: string;
|
||||
status: string;
|
||||
remarks: string;
|
||||
projects: string[];
|
||||
platform: Platform;
|
||||
};
|
||||
|
||||
export type OrganizationList = {
|
||||
teams: Organization[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
|
||||
export type BillingLimits = {
|
||||
bandwidth: number;
|
||||
documents: number;
|
||||
executions: number;
|
||||
storage: number;
|
||||
users: number;
|
||||
budgetLimit: number;
|
||||
};
|
||||
import { type Models, Platform, Query } from '@appwrite.io/console';
|
||||
|
||||
export const newOrgModal = writable<boolean>(false);
|
||||
export const newMemberModal = writable<boolean>(false);
|
||||
@@ -62,7 +11,26 @@ export const organizationList = derived(
|
||||
($page) => $page.data.organizations as Models.TeamList<Record<string, unknown>>
|
||||
);
|
||||
|
||||
export const organization = derived(page, ($page) => $page.data?.organization as Organization);
|
||||
export const currentPlan = derived(page, ($page) => $page.data?.currentPlan as Plan);
|
||||
export const organization = derived(
|
||||
page,
|
||||
($page) => $page.data?.organization as Models.Organization
|
||||
);
|
||||
export const currentPlan = derived(page, ($page) => $page.data?.currentPlan as Models.BillingPlan);
|
||||
export const members = derived(page, ($page) => $page.data.members as Models.MembershipList);
|
||||
export const regions = writable<Models.ConsoleRegionList>({ total: 0, regions: [] });
|
||||
|
||||
export async function getTeamOrOrganizationList(
|
||||
queries: string[] = []
|
||||
): Promise<Models.TeamList | Models.OrganizationList> {
|
||||
let organizations: Models.TeamList | Models.OrganizationList;
|
||||
|
||||
if (isCloud) {
|
||||
organizations = await sdk.forConsole.organizations.list({
|
||||
queries: [...queries, Query.equal('platform', Platform.Appwrite)]
|
||||
});
|
||||
} else {
|
||||
organizations = await sdk.forConsole.teams.list({ queries });
|
||||
}
|
||||
|
||||
return organizations;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
Realtime,
|
||||
Organizations
|
||||
} from '@appwrite.io/console';
|
||||
import { Billing } from '../sdk/billing';
|
||||
import { Sources } from '$lib/sdk/sources';
|
||||
import {
|
||||
REGION_FRA,
|
||||
@@ -92,7 +91,6 @@ function createConsoleSdk(client: Client) {
|
||||
migrations: new Migrations(client),
|
||||
console: new Console(client),
|
||||
assistant: new Assistant(client),
|
||||
billing: new Billing(client),
|
||||
sources: new Sources(client),
|
||||
sites: new Sites(client),
|
||||
domains: new Domains(client),
|
||||
|
||||
+37
-30
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
Appearance,
|
||||
PaymentMethod,
|
||||
PaymentMethod as StripePaymentMethod,
|
||||
Stripe,
|
||||
StripeElement,
|
||||
StripeElements
|
||||
@@ -8,21 +8,21 @@ import type {
|
||||
import { sdk } from './sdk';
|
||||
import { app } from './app';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from './notifications';
|
||||
import { organization } from './organization';
|
||||
import { base } from '$app/paths';
|
||||
import { resolve } from '$app/paths';
|
||||
import { ThemeDarkCloud, ThemeLightCloud } from '$themes';
|
||||
import Color from 'color';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const stripe = writable<Stripe>();
|
||||
let paymentMethod: PaymentMethodData;
|
||||
export const isStripeInitialized = writable(false);
|
||||
|
||||
let clientSecret: string;
|
||||
let elements: StripeElements;
|
||||
let paymentElement: StripeElement;
|
||||
|
||||
export const isStripeInitialized = writable(false);
|
||||
let paymentMethod: Models.PaymentMethod;
|
||||
|
||||
export async function initializeStripe(node: HTMLElement) {
|
||||
if (!get(stripe)) return;
|
||||
@@ -32,7 +32,7 @@ export async function initializeStripe(node: HTMLElement) {
|
||||
|
||||
isStripeInitialized.set(true);
|
||||
|
||||
const methods = await sdk.forConsole.billing.listPaymentMethods();
|
||||
const methods = await sdk.forConsole.account.listPaymentMethods();
|
||||
|
||||
// Get the client secret from empty payment method if available
|
||||
clientSecret = methods.paymentMethods?.filter(
|
||||
@@ -41,7 +41,7 @@ export async function initializeStripe(node: HTMLElement) {
|
||||
|
||||
// If there is no payment method, create an empty one and get the client secret
|
||||
if (!clientSecret) {
|
||||
paymentMethod = await sdk.forConsole.billing.createPaymentMethod();
|
||||
paymentMethod = await sdk.forConsole.account.createPaymentMethod();
|
||||
clientSecret = paymentMethod.clientSecret;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export async function submitStripeCard(name: string, organizationId?: string) {
|
||||
try {
|
||||
// If a payment method was created during initialization, use it, otherwise create a new one
|
||||
if (!paymentMethod) {
|
||||
paymentMethod = await sdk.forConsole.billing.createPaymentMethod();
|
||||
paymentMethod = await sdk.forConsole.account.createPaymentMethod();
|
||||
clientSecret = paymentMethod.clientSecret;
|
||||
}
|
||||
|
||||
@@ -116,12 +116,12 @@ export async function submitStripeCard(name: string, organizationId?: string) {
|
||||
}
|
||||
|
||||
if (setupIntent && setupIntent.status === 'succeeded') {
|
||||
const pm = setupIntent.payment_method as PaymentMethod | string | undefined;
|
||||
const pm = setupIntent.payment_method as StripePaymentMethod | string | undefined;
|
||||
// If Stripe returned an expanded PaymentMethod object, check the card country.
|
||||
// If it returned a string id (common), `typeof pm === 'string'` and we skip this.
|
||||
if (typeof pm !== 'string' && pm?.card?.country === 'US') {
|
||||
// need to get state
|
||||
return pm as PaymentMethod;
|
||||
return pm as StripePaymentMethod;
|
||||
}
|
||||
|
||||
// The backend expects a provider method ID (string). Extract the id
|
||||
@@ -130,7 +130,7 @@ export async function submitStripeCard(name: string, organizationId?: string) {
|
||||
if (typeof pm === 'string') {
|
||||
providerId = pm;
|
||||
} else {
|
||||
providerId = (pm as PaymentMethod)?.id;
|
||||
providerId = (pm as StripePaymentMethod)?.id;
|
||||
}
|
||||
|
||||
if (!providerId) {
|
||||
@@ -139,11 +139,12 @@ export async function submitStripeCard(name: string, organizationId?: string) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
const method = await sdk.forConsole.billing.setPaymentMethod(
|
||||
paymentMethod.$id,
|
||||
providerId,
|
||||
const method = await sdk.forConsole.account.updatePaymentMethodProvider({
|
||||
paymentMethodId: paymentMethod.$id,
|
||||
providerMethodId: providerId,
|
||||
name
|
||||
);
|
||||
});
|
||||
|
||||
paymentElement.destroy();
|
||||
isStripeInitialized.set(false);
|
||||
trackEvent(Submit.PaymentMethodCreate);
|
||||
@@ -169,12 +170,12 @@ export async function setPaymentMethod(providerMethodId: string, name: string, s
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const method = await sdk.forConsole.billing.setPaymentMethod(
|
||||
paymentMethod.$id,
|
||||
providerMethodId,
|
||||
const method = await sdk.forConsole.account.updatePaymentMethodProvider({
|
||||
paymentMethodId: paymentMethod.$id,
|
||||
providerMethodId: providerMethodId,
|
||||
name,
|
||||
state
|
||||
);
|
||||
});
|
||||
paymentElement.destroy();
|
||||
isStripeInitialized.set(false);
|
||||
trackEvent(Submit.PaymentMethodCreate);
|
||||
@@ -185,17 +186,22 @@ export async function setPaymentMethod(providerMethodId: string, name: string, s
|
||||
}
|
||||
}
|
||||
|
||||
export async function confirmPayment(
|
||||
orgId: string,
|
||||
clientSecret: string,
|
||||
paymentMethodId: string,
|
||||
route?: string
|
||||
) {
|
||||
try {
|
||||
const url =
|
||||
window.location.origin + (route ? route : `${base}/organization-${orgId}/billing`);
|
||||
export async function confirmPayment(config: {
|
||||
clientSecret: string;
|
||||
paymentMethodId: string;
|
||||
orgId?: string;
|
||||
route?: string;
|
||||
}) {
|
||||
const { clientSecret, paymentMethodId, orgId, route } = config;
|
||||
|
||||
const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentMethodId);
|
||||
try {
|
||||
const resolvedUrl = resolve('/(console)/organization-[organization]/billing', {
|
||||
organization: orgId
|
||||
});
|
||||
|
||||
const url = window.location.origin + (route ? route : resolvedUrl);
|
||||
|
||||
const paymentMethod = await sdk.forConsole.account.getPaymentMethod({ paymentMethodId });
|
||||
|
||||
const { error } = await get(stripe).confirmPayment({
|
||||
clientSecret: clientSecret,
|
||||
@@ -204,6 +210,7 @@ export async function confirmPayment(
|
||||
payment_method: paymentMethod.providerMethodId
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error.message;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { BillingPlan, INTERVAL } from '$lib/constants';
|
||||
import { INTERVAL } from '$lib/constants';
|
||||
import Footer from '$lib/layout/footer.svelte';
|
||||
import Shell from '$lib/layout/shell.svelte';
|
||||
|
||||
import { app } from '$lib/stores/app';
|
||||
import { database, checkForDatabaseBackupPolicies } from '$lib/stores/database';
|
||||
import { newOrgModal, organization, type Organization } from '$lib/stores/organization';
|
||||
import { newOrgModal, organization } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { afterUpdate, onMount } from 'svelte';
|
||||
import { loading } from '$routes/store';
|
||||
@@ -22,7 +22,6 @@
|
||||
checkForUsageLimit,
|
||||
checkPaymentAuthorizationRequired,
|
||||
paymentExpired,
|
||||
plansInfo,
|
||||
showUsageRatesModal
|
||||
} from '$lib/stores/billing';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -56,6 +55,7 @@
|
||||
IconSwitchHorizontal
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
});
|
||||
|
||||
let currentOrganizationId = null;
|
||||
async function checkForUsageLimits(org: Organization) {
|
||||
async function checkForUsageLimits(org: Models.Organization) {
|
||||
if (!org) return;
|
||||
if (currentOrganizationId === org.$id) return;
|
||||
if (isCloud) {
|
||||
@@ -304,11 +304,11 @@
|
||||
checkForMarkedForDeletion(org);
|
||||
await checkForNewDevUpgradePro(org);
|
||||
|
||||
if (org?.billingPlan !== BillingPlan.FREE) {
|
||||
if (org?.billingPlanDetails.requiresPaymentMethod) {
|
||||
await paymentExpired(org);
|
||||
await checkPaymentAuthorizationRequired(org);
|
||||
|
||||
if ($plansInfo.get(org.billingPlan)?.trialDays) {
|
||||
if (org?.billingTrialDays) {
|
||||
calculateTrialDay(org);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import type { Tier } from '$lib/stores/billing';
|
||||
import type { Plan, PlanList } from '$lib/sdk/billing';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Platform, Query } from '@appwrite.io/console';
|
||||
import { makePlansMap } from '$lib/helpers/billing';
|
||||
import { plansInfo as plansInfoStore } from '$lib/stores/billing';
|
||||
|
||||
export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
const { organizations } = await parent();
|
||||
const { organizations, plansInfo } = await parent();
|
||||
|
||||
depends(Dependencies.RUNTIMES);
|
||||
depends(Dependencies.CONSOLE_VARIABLES);
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
|
||||
const { endpoint, project } = sdk.forConsole.client.config;
|
||||
|
||||
const plansArrayPromise =
|
||||
plansInfo || !isCloud
|
||||
? null
|
||||
: sdk.forConsole.console.getPlans({
|
||||
platform: Platform.Appwrite
|
||||
});
|
||||
|
||||
const [preferences, plansArray, versionData, consoleVariables] = await Promise.all([
|
||||
sdk.forConsole.account.getPrefs(),
|
||||
isCloud ? sdk.forConsole.billing.getPlansInfo() : null,
|
||||
plansArrayPromise,
|
||||
fetch(`${endpoint}/health/version`, {
|
||||
headers: { 'X-Appwrite-Project': project as string }
|
||||
}).then((response) => response.json() as { version?: string }),
|
||||
sdk.forConsole.console.variables()
|
||||
]);
|
||||
|
||||
const plansInfo = toPlanMap(plansArray);
|
||||
let fallbackPlansInfoArray = plansInfo;
|
||||
if (!fallbackPlansInfoArray) {
|
||||
fallbackPlansInfoArray = makePlansMap(plansArray);
|
||||
}
|
||||
|
||||
const currentOrgId =
|
||||
preferences.organization ??
|
||||
@@ -47,28 +58,18 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// just in case!
|
||||
plansInfoStore.set(fallbackPlansInfoArray);
|
||||
|
||||
return {
|
||||
plansInfo,
|
||||
roles: [],
|
||||
scopes: [],
|
||||
preferences,
|
||||
currentOrgId,
|
||||
organizations,
|
||||
consoleVariables,
|
||||
version: versionData?.version ?? null,
|
||||
allProjectsCount: projectsCount
|
||||
allProjectsCount: projectsCount,
|
||||
plansInfo: fallbackPlansInfoArray,
|
||||
version: versionData?.version ?? null
|
||||
};
|
||||
};
|
||||
|
||||
function toPlanMap(plansArray: PlanList | null): Map<Tier, Plan> {
|
||||
const map = new Map<Tier, Plan>();
|
||||
if (!plansArray?.plans.length) return map;
|
||||
|
||||
const plans = plansArray.plans;
|
||||
for (let i = 0; i < plans.length; i++) {
|
||||
const plan = plans[i];
|
||||
map.set(plan.$id as Tier, plan);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Badge, Skeleton } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { daysLeftInTrial, plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { daysLeftInTrial, billingIdToPlan } from '$lib/stores/billing';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Icon, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
@@ -40,16 +38,18 @@
|
||||
if (!billingPlan) return 'Unknown';
|
||||
|
||||
// For known plans, use tierToPlan
|
||||
const tierData = tierToPlan(billingPlan as Tier);
|
||||
const tierData = billingIdToPlan(billingPlan);
|
||||
|
||||
// If it's not a custom plan or we got a non-custom result, return the name
|
||||
// If it's not a custom plan, or we got a non-custom result, return the name
|
||||
if (tierData.name !== 'Custom') {
|
||||
return tierData.name;
|
||||
}
|
||||
|
||||
// For custom plans, fetch from API
|
||||
try {
|
||||
const plan = await sdk.forConsole.billing.getPlan(billingPlan);
|
||||
const plan = await sdk.forConsole.console.getPlan({
|
||||
planId: billingPlan
|
||||
});
|
||||
return plan.name;
|
||||
} catch (error) {
|
||||
// Fallback to 'Custom' if fetch fails
|
||||
@@ -57,33 +57,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isOrganizationOnTrial(organization: Organization): boolean {
|
||||
function isOrganizationOnTrial(organization: Models.Organization): boolean {
|
||||
if (!organization?.billingTrialStartDate) return false;
|
||||
if ($daysLeftInTrial <= 0) return false;
|
||||
if (organization.billingPlan === BillingPlan.FREE) return false;
|
||||
if (!organization.billingPlanDetails.trial) return false;
|
||||
|
||||
return !!$plansInfo.get(organization.billingPlan)?.trialDays;
|
||||
return !!organization?.billingTrialDays;
|
||||
}
|
||||
|
||||
function isNonPayingOrganization(organization: Organization): boolean {
|
||||
return (
|
||||
organization?.billingPlan === BillingPlan.FREE ||
|
||||
organization?.billingPlan === BillingPlan.GITHUB_EDUCATION
|
||||
);
|
||||
function isNonPayingOrganization(organization: Models.Organization): boolean {
|
||||
// plan doesn't require payments, it is a non-paying org!
|
||||
return !organization?.billingPlanDetails.requiresPaymentMethod;
|
||||
}
|
||||
|
||||
function isPayingOrganization(team: Models.Preferences | Organization): Organization | null {
|
||||
function isPayingOrganization(
|
||||
team: Models.Preferences | Models.Organization
|
||||
): Models.Organization | null {
|
||||
const isPayingOrganization =
|
||||
isCloudOrg(team) && !isOrganizationOnTrial(team) && !isNonPayingOrganization(team);
|
||||
|
||||
if (isPayingOrganization) return team as Organization;
|
||||
if (isPayingOrganization) return team as Models.Organization;
|
||||
else return null;
|
||||
}
|
||||
|
||||
function isCloudOrg(
|
||||
data: Partial<Models.TeamList<Models.Preferences>> | Organization
|
||||
): data is Organization {
|
||||
return isCloud && 'billingPlan' in data;
|
||||
data: Partial<Models.TeamList<Models.Preferences>> | Models.Organization
|
||||
): data is Models.Organization {
|
||||
return isCloud && 'billingPlanId' in data;
|
||||
}
|
||||
|
||||
function createOrg() {
|
||||
@@ -114,7 +114,7 @@
|
||||
{@const avatarList = getMemberships(organization.$id)}
|
||||
{@const payingOrg = isPayingOrganization(organization)}
|
||||
{@const planName = isCloudOrg(organization)
|
||||
? getPlanName(organization.billingPlan)
|
||||
? getPlanName(organization.billingPlanId)
|
||||
: null}
|
||||
|
||||
<GridItem1 href={`${base}/organization-${organization.$id}`}>
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { Query, Platform } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
import { getTeamOrOrganizationList } from '$lib/stores/organization';
|
||||
|
||||
export const load: PageLoad = async ({ url, route }) => {
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, CARD_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
|
||||
const queries = [
|
||||
Query.offset(offset),
|
||||
Query.limit(limit),
|
||||
Query.orderDesc(''),
|
||||
...(isCloud ? [Query.equal('platform', Platform.Appwrite)] : [])
|
||||
];
|
||||
const queries = [Query.offset(offset), Query.limit(limit), Query.orderDesc('')];
|
||||
|
||||
const organizations = !isCloud
|
||||
? await sdk.forConsole.teams.list({ queries })
|
||||
: await sdk.forConsole.billing.listOrganization(queries);
|
||||
const organizations = await getTeamOrOrganizationList(queries);
|
||||
|
||||
return {
|
||||
offset,
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
if (page.url.searchParams.has('clientSecret')) {
|
||||
const clientSecret = page.url.searchParams.get('clientSecret');
|
||||
const paymentMethodId = page.url.searchParams.get('paymentMethodId');
|
||||
await confirmPayment('', clientSecret, paymentMethodId);
|
||||
if (clientSecret && paymentMethodId) {
|
||||
await confirmPayment({ clientSecret, paymentMethodId });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,8 +7,8 @@ export const load: PageLoad = async ({ depends }) => {
|
||||
depends(Dependencies.ADDRESS);
|
||||
|
||||
const [paymentMethods, addressList, countryList, locale] = await Promise.all([
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
sdk.forConsole.account.listPaymentMethods(),
|
||||
sdk.forConsole.account.listBillingAddresses(),
|
||||
sdk.forConsole.locale.listCountries(),
|
||||
sdk.forConsole.locale.get()
|
||||
]);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
@@ -43,18 +42,22 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const response = await sdk.forConsole.billing.createAddress(
|
||||
const response = await sdk.forConsole.account.createBillingAddress({
|
||||
country,
|
||||
address,
|
||||
streetAddress: address,
|
||||
city,
|
||||
state,
|
||||
zip ? zip : undefined,
|
||||
address2 ? address2 : undefined
|
||||
);
|
||||
postalCode: zip ? zip : undefined,
|
||||
addressLine2: address2 ? address2 : undefined
|
||||
});
|
||||
|
||||
trackEvent(Submit.BillingAddressCreate);
|
||||
let org: Organization = null;
|
||||
let org: Models.Organization = null;
|
||||
if (organization) {
|
||||
org = await sdk.forConsole.billing.setBillingAddress(organization, response.$id);
|
||||
org = await sdk.forConsole.organizations.setBillingAddress({
|
||||
organizationId: organization,
|
||||
billingAddressId: response.$id
|
||||
});
|
||||
trackEvent(Submit.OrganizationBillingAddressUpdate);
|
||||
await invalidate(Dependencies.ORGANIZATIONS);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import DeleteAddress from './deleteAddressModal.svelte';
|
||||
import EditAddressModal from './editAddressModal.svelte';
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import { organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { organizationList } from '$lib/stores/organization';
|
||||
import { base } from '$app/paths';
|
||||
import {
|
||||
IconDotsHorizontal,
|
||||
@@ -35,11 +34,11 @@
|
||||
|
||||
let show = false;
|
||||
let showEdit = false;
|
||||
let selectedAddress: Address;
|
||||
let selectedLinkedOrgs: Organization[] = [];
|
||||
let selectedAddress: Models.BillingAddress;
|
||||
let selectedLinkedOrgs: Array<Models.Organization> = [];
|
||||
let showDelete = false;
|
||||
|
||||
$: orgList = $organizationList.teams as unknown as Organization[];
|
||||
$: orgList = $organizationList.teams as unknown as Array<Models.Organization>;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
|
||||
@@ -4,20 +4,23 @@
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Layout, Link } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let showDelete = false;
|
||||
export let selectedAddress: Address;
|
||||
export let linkedOrgs: Organization[] = [];
|
||||
export let linkedOrgs: Array<Models.Organization> = [];
|
||||
export let selectedAddress: Models.BillingAddress;
|
||||
|
||||
let error: string = null;
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await sdk.forConsole.billing.deleteAddress(selectedAddress.$id);
|
||||
await sdk.forConsole.account.deleteBillingAddress({
|
||||
billingAddressId: selectedAddress.$id
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
|
||||
@@ -5,19 +5,21 @@
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Layout, Link, Typography } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let linkedOrgs: Organization[] = [];
|
||||
export let showDelete = false;
|
||||
export let method: string;
|
||||
export let showDelete = false;
|
||||
export let linkedOrgs: Array<Models.Organization> = [];
|
||||
|
||||
let error: string;
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await sdk.forConsole.billing.deletePaymentMethod(method);
|
||||
await sdk.forConsole.account.deletePaymentMethod({
|
||||
paymentMethodId: method
|
||||
});
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
|
||||
@@ -4,16 +4,15 @@
|
||||
import { Modal } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let selectedAddress: Address;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
export let selectedAddress: Models.BillingAddress;
|
||||
|
||||
let error: string = null;
|
||||
let options = [
|
||||
@@ -34,20 +33,22 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await sdk.forConsole.billing.updateAddress(
|
||||
selectedAddress.$id,
|
||||
selectedAddress.country,
|
||||
selectedAddress.streetAddress,
|
||||
selectedAddress.city,
|
||||
selectedAddress.state,
|
||||
selectedAddress.postalCode ? selectedAddress.postalCode : undefined,
|
||||
selectedAddress.addressLine2 ? selectedAddress.addressLine2 : undefined
|
||||
);
|
||||
await sdk.forConsole.account.updateBillingAddress({
|
||||
billingAddressId: selectedAddress.$id,
|
||||
country: selectedAddress.country,
|
||||
streetAddress: selectedAddress.streetAddress,
|
||||
city: selectedAddress.city,
|
||||
state: selectedAddress.state,
|
||||
postalCode: selectedAddress.postalCode ? selectedAddress.postalCode : undefined,
|
||||
addressLine2: selectedAddress.addressLine2
|
||||
? selectedAddress.addressLine2
|
||||
: undefined
|
||||
});
|
||||
await invalidate(Dependencies.ADDRESS);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Address has been added`
|
||||
message: 'Address has been updated'
|
||||
});
|
||||
trackEvent(Submit.BillingAddressUpdate);
|
||||
} catch (e) {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Alert } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let {
|
||||
show = $bindable(false),
|
||||
@@ -16,7 +16,7 @@
|
||||
}: {
|
||||
show: boolean;
|
||||
isLinked?: boolean;
|
||||
selectedPaymentMethod: PaymentMethodData;
|
||||
selectedPaymentMethod: Models.PaymentMethod;
|
||||
} = $props();
|
||||
|
||||
let year: number | null = $state(null);
|
||||
@@ -24,6 +24,7 @@
|
||||
let error: string | null = $state(null);
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const months = Array.from({ length: 12 }, (_, i) => {
|
||||
const value = String(i + 1).padStart(2, '0');
|
||||
return { value, label: value };
|
||||
@@ -33,11 +34,14 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await sdk.forConsole.billing.updatePaymentMethod(
|
||||
selectedPaymentMethod.$id,
|
||||
month,
|
||||
year?.toString()
|
||||
);
|
||||
await sdk.forConsole.account.updatePaymentMethod({
|
||||
paymentMethodId: selectedPaymentMethod.$id,
|
||||
expiryMonth: parseInt(month),
|
||||
expiryYear: year
|
||||
});
|
||||
|
||||
trackEvent(Submit.PaymentMethodUpdate);
|
||||
invalidate(Dependencies.PAYMENT_METHODS);
|
||||
show = false;
|
||||
trackEvent(Submit.PaymentMethodUpdate);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import { CardGrid, CreditCardInfo, Empty } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { paymentMethods } from '$lib/stores/billing';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { organizationList } from '$lib/stores/organization';
|
||||
import { base } from '$app/paths';
|
||||
import EditPaymentModal from './editPaymentModal.svelte';
|
||||
import DeletePaymentModal from './deletePaymentModal.svelte';
|
||||
@@ -27,24 +26,23 @@
|
||||
Tag,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let showPayment = false;
|
||||
let showDropdown = [];
|
||||
let selectedMethod: PaymentMethodData;
|
||||
let selectedLinkedOrgs: Organization[] = [];
|
||||
let selectedMethod: Models.PaymentMethod;
|
||||
let selectedLinkedOrgs: Array<Models.Organization> = [];
|
||||
let showDelete = false;
|
||||
let showEdit = false;
|
||||
let showUpdateState = false;
|
||||
let paymentMethodNeedingState: PaymentMethodData | null = null;
|
||||
let paymentMethodNeedingState: Models.PaymentMethod | null = null;
|
||||
let isLinked = false;
|
||||
|
||||
$: orgList = $organizationList.teams as unknown as Organization[];
|
||||
$: orgList = $organizationList.teams as unknown as Array<Models.Organization>;
|
||||
|
||||
$: filteredMethods = $paymentMethods?.paymentMethods.filter(
|
||||
(method: PaymentMethodData) => !!method?.last4
|
||||
);
|
||||
$: filteredMethods = $paymentMethods?.paymentMethods.filter((method) => !!method?.last4);
|
||||
|
||||
const isMethodLinkedToOrg = (methodId: string, org: Organization) =>
|
||||
const isMethodLinkedToOrg = (methodId: string, org: Models.Organization) =>
|
||||
methodId === org.paymentMethodId || methodId === org.backupPaymentMethodId;
|
||||
|
||||
$: linkedMethodIds = new Set(
|
||||
@@ -57,7 +55,7 @@
|
||||
$: {
|
||||
if ($paymentMethods?.paymentMethods && !showUpdateState && !paymentMethodNeedingState) {
|
||||
const usMethodWithoutState = $paymentMethods.paymentMethods.find(
|
||||
(method: PaymentMethodData) =>
|
||||
(method: Models.PaymentMethod) =>
|
||||
method?.country?.toLowerCase() === 'us' &&
|
||||
(!method.state || method.state.trim() === '') &&
|
||||
!!method.last4
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { base, resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CreditsApplied, SelectPaymentMethod } from '$lib/components/billing';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputSelect, InputTags, InputText } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import type { PaymentList, Plan } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import {
|
||||
organizationList,
|
||||
type Organization,
|
||||
type OrganizationError
|
||||
} from '$lib/stores/organization';
|
||||
import { organizationList } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmPayment } from '$lib/stores/stripe.js';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { BillingPlanGroup, ID, type Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { isOrganization, plansInfo, type Tier } from '$lib/stores/billing';
|
||||
import {
|
||||
billingIdToPlan,
|
||||
getBasePlanFromGroup,
|
||||
isPaymentAuthenticationRequired,
|
||||
plansInfo
|
||||
} from '$lib/stores/billing';
|
||||
import { Fieldset, Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte';
|
||||
@@ -48,7 +48,7 @@
|
||||
let formComponent: Form;
|
||||
let couponForm: Form;
|
||||
let isSubmitting = writable(false);
|
||||
let methods: PaymentList;
|
||||
let methods: Models.PaymentMethodList;
|
||||
let paymentMethodId: string;
|
||||
let collaborators: string[];
|
||||
let taxId: string;
|
||||
@@ -68,16 +68,18 @@
|
||||
let coupon: string;
|
||||
let couponData = data?.couponData;
|
||||
let campaign = data?.campaign;
|
||||
let billingPlan: Tier = BillingPlan.PRO;
|
||||
let tempOrgId = null;
|
||||
let currentPlan: Plan;
|
||||
|
||||
let billingPlan = getBasePlanFromGroup(BillingPlanGroup.Pro);
|
||||
|
||||
let currentPlan: Models.BillingPlan;
|
||||
|
||||
$: onlyNewOrgs = campaign?.onlyNewOrgs || couponData?.onlyNewOrgs;
|
||||
|
||||
$: selectedOrgId = tempOrgId;
|
||||
|
||||
function isUpgrade() {
|
||||
const newPlan = $plansInfo.get(billingPlan);
|
||||
const newPlan = $plansInfo.get(billingPlan.$id);
|
||||
return currentPlan && newPlan && currentPlan.order < newPlan.order;
|
||||
}
|
||||
|
||||
@@ -86,12 +88,15 @@
|
||||
if (!$organizationList?.total || campaign?.onlyNewOrgs) {
|
||||
selectedOrgId = newOrgId;
|
||||
}
|
||||
|
||||
if (page.url.searchParams.has('org')) {
|
||||
selectedOrgId = page.url.searchParams.get('org');
|
||||
tempOrgId = selectedOrgId;
|
||||
canSelectOrg = false;
|
||||
}
|
||||
|
||||
if (campaign?.plan) {
|
||||
billingPlan = campaign.plan;
|
||||
billingPlan = billingIdToPlan(campaign.plan);
|
||||
}
|
||||
|
||||
if ($organizationList.total > 0 && tempOrgId === null) {
|
||||
@@ -100,10 +105,17 @@
|
||||
if (page.url.searchParams.has('type')) {
|
||||
const type = page.url.searchParams.get('type');
|
||||
if (type === 'payment_confirmed') {
|
||||
const organizationId = page.url.searchParams.get('id');
|
||||
collaborators = page.url.searchParams.get('invites').split(',');
|
||||
const organizationId =
|
||||
page.url.searchParams.get('org') || page.url.searchParams.get('id');
|
||||
const invites = page.url.searchParams.get('invites');
|
||||
if (invites) {
|
||||
collaborators = invites.split(',');
|
||||
}
|
||||
|
||||
await sdk.forConsole.billing.validateOrganization(organizationId, collaborators);
|
||||
await sdk.forConsole.organizations.validatePayment({
|
||||
organizationId,
|
||||
invites: collaborators
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +125,7 @@
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
const methodList = await sdk.forConsole.billing.listPaymentMethods();
|
||||
const methodList = await sdk.forConsole.account.listPaymentMethods();
|
||||
const filteredMethods = methodList.paymentMethods.filter((method) => !!method?.last4);
|
||||
methods = { paymentMethods: filteredMethods, total: filteredMethods.length };
|
||||
paymentMethodId =
|
||||
@@ -126,60 +138,66 @@
|
||||
if (couponForm && !couponForm.checkValidity()) return;
|
||||
isSubmitting.set(true);
|
||||
try {
|
||||
let org: Organization | OrganizationError;
|
||||
let org: Models.Organization | Models.PaymentAuthentication;
|
||||
// Create new org
|
||||
if (selectedOrgId === newOrgId) {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
newOrgId,
|
||||
org = await sdk.forConsole.organizations.create({
|
||||
organizationId: newOrgId,
|
||||
name,
|
||||
billingPlan,
|
||||
billingPlan: billingPlan.$id,
|
||||
paymentMethodId,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
taxId
|
||||
);
|
||||
invites: collaborators,
|
||||
couponId: couponData.code ? couponData.code : null,
|
||||
taxId,
|
||||
budget: billingBudget
|
||||
});
|
||||
}
|
||||
|
||||
// Upgrade existing org
|
||||
else if (selectedOrg?.billingPlan !== billingPlan && isUpgrade()) {
|
||||
org = await sdk.forConsole.billing.updatePlan(
|
||||
selectedOrg.$id,
|
||||
billingPlan,
|
||||
else if (selectedOrg?.billingPlanId !== billingPlan.$id && isUpgrade()) {
|
||||
org = await sdk.forConsole.organizations.updatePlan({
|
||||
organizationId: selectedOrg.$id,
|
||||
billingPlan: billingPlan.$id,
|
||||
paymentMethodId,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators
|
||||
);
|
||||
invites: collaborators,
|
||||
couponId: couponData.code ? couponData.code : null
|
||||
});
|
||||
}
|
||||
// Existing pro org, apply credits
|
||||
else {
|
||||
org = selectedOrg;
|
||||
await sdk.forConsole.billing.addCredit(org.$id, couponData.code);
|
||||
await sdk.forConsole.organizations.addCredit({
|
||||
organizationId: org.$id,
|
||||
couponId: couponData.code
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOrganization(org) && org.status === 402) {
|
||||
if (isPaymentAuthenticationRequired(org)) {
|
||||
let clientSecret = org.clientSecret;
|
||||
let params = new URLSearchParams();
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('org', org.teamId);
|
||||
params.append('org', org.organizationId);
|
||||
for (const [key, value] of page.url.searchParams.entries()) {
|
||||
if (key !== 'type' && key !== 'id') {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
params.append('invites', collaborators.join(','));
|
||||
await confirmPayment(
|
||||
'',
|
||||
const resolvedUrl = resolve('/(console)/apply-credit');
|
||||
|
||||
await confirmPayment({
|
||||
clientSecret,
|
||||
paymentMethodId,
|
||||
`${base}/apply-credit?${params}`
|
||||
);
|
||||
org = await sdk.forConsole.billing.validateOrganization(org.teamId, collaborators);
|
||||
route: `${resolvedUrl}?${params}`
|
||||
});
|
||||
|
||||
org = await sdk.forConsole.organizations.validatePayment({
|
||||
organizationId: org.organizationId,
|
||||
invites: collaborators
|
||||
});
|
||||
}
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
trackEvent(Submit.CreditRedeem, {
|
||||
coupon: couponData.code,
|
||||
campaign: couponData?.campaign
|
||||
@@ -205,8 +223,10 @@
|
||||
|
||||
async function addCoupon() {
|
||||
try {
|
||||
const response = await sdk.forConsole.billing.getCouponAccount(coupon);
|
||||
couponData = response;
|
||||
couponData = await sdk.forConsole.console.getCoupon({
|
||||
couponId: coupon
|
||||
});
|
||||
|
||||
coupon = null;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
@@ -222,21 +242,21 @@
|
||||
|
||||
$: selectedOrg = $organizationList?.teams?.find(
|
||||
(team) => team.$id === selectedOrgId
|
||||
) as Organization;
|
||||
) as Models.Organization;
|
||||
|
||||
function getBillingPlan(): Tier | undefined {
|
||||
const campaignPlan =
|
||||
campaign?.plan && $plansInfo.get(campaign.plan) ? $plansInfo.get(campaign.plan) : null;
|
||||
const newPlan = $plansInfo.get(billingPlan);
|
||||
function getBillingPlan(): Models.BillingPlan | undefined {
|
||||
const newPlan = billingIdToPlan(billingPlan.$id);
|
||||
const planInCache = billingIdToPlan(campaign.plan);
|
||||
const campaignPlan = campaign?.plan && planInCache ? planInCache : null;
|
||||
|
||||
// if campaign has a plan and it's higher than the selected new plan
|
||||
// if campaign has a plan, and it's higher than the selected new plan
|
||||
if (campaignPlan?.order > newPlan?.order) {
|
||||
return campaignPlan.$id as Tier;
|
||||
return campaignPlan;
|
||||
}
|
||||
|
||||
// if current plan's order is higher than the selected new plan
|
||||
if (currentPlan?.order > newPlan?.order) {
|
||||
return currentPlan.$id as Tier;
|
||||
return currentPlan;
|
||||
}
|
||||
|
||||
return billingPlan;
|
||||
@@ -250,7 +270,9 @@
|
||||
|
||||
$: if (!isNewOrg) {
|
||||
(async () => {
|
||||
currentPlan = await sdk.forConsole.billing.getOrganizationPlan(selectedOrgId);
|
||||
currentPlan = await sdk.forConsole.organizations.getPlan({
|
||||
organizationId: selectedOrgId
|
||||
});
|
||||
})();
|
||||
loadPaymentMethods();
|
||||
}
|
||||
@@ -262,6 +284,15 @@
|
||||
) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
|
||||
/* check if payment method selection is needed */
|
||||
$: needsPaymentMethod = selectedOrgId && selectedOrg?.billingPlanDetails.requiresPaymentMethod;
|
||||
|
||||
/* check if coupon code input should be shown */
|
||||
$: needsCouponInput = !data?.couponData?.code && selectedOrgId;
|
||||
|
||||
/* show payment section if either payment method or coupon input is needed */
|
||||
$: showPaymentSection = needsPaymentMethod || needsCouponInput;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -283,7 +314,9 @@
|
||||
placeholder="Select organization"
|
||||
id="organization" />
|
||||
{/if}
|
||||
{#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)}
|
||||
|
||||
<!-- show invite members -->
|
||||
{#if selectedOrgId && !selectedOrg?.billingPlanDetails.addons.seats.supported}
|
||||
{#if selectedOrgId === newOrgId}
|
||||
<InputText
|
||||
label="Organization name"
|
||||
@@ -292,6 +325,7 @@
|
||||
required
|
||||
bind:value={name} />
|
||||
{/if}
|
||||
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
@@ -309,17 +343,18 @@
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
{#if (selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)) || (!data?.couponData?.code && selectedOrgId)}
|
||||
|
||||
{#if showPaymentSection}
|
||||
<Fieldset legend="Payment">
|
||||
<Layout.Stack gap="xl">
|
||||
{#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)}
|
||||
{#if needsPaymentMethod}
|
||||
<SelectPaymentMethod
|
||||
bind:methods
|
||||
bind:value={paymentMethodId}
|
||||
bind:taxId />
|
||||
{/if}
|
||||
<Form bind:this={couponForm} onSubmit={addCoupon}>
|
||||
{#if !data?.couponData?.code && selectedOrgId}
|
||||
{#if needsCouponInput}
|
||||
<Layout.Stack gap="s" direction="row" alignItems="flex-end">
|
||||
<InputText
|
||||
required
|
||||
@@ -347,7 +382,7 @@
|
||||
</Form>
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if selectedOrg?.$id && selectedOrg?.billingPlan === billingPlan}
|
||||
{#if selectedOrg?.$id && selectedOrg?.billingPlanId === billingPlan.$id}
|
||||
<section
|
||||
class="card"
|
||||
style:--p-card-padding="1.5rem"
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
import { base } from '$app/paths';
|
||||
import type { Coupon } from '$lib/sdk/billing.js';
|
||||
import type { Campaign } from '$lib/stores/campaigns.js';
|
||||
import { resolve } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const load = async ({ url }) => {
|
||||
// Has promo code
|
||||
if (url.searchParams.has('code')) {
|
||||
let couponData: Coupon;
|
||||
let campaign: Campaign;
|
||||
let couponData: Models.Coupon;
|
||||
let campaign: Models.Campaign;
|
||||
const code = url.searchParams.get('code');
|
||||
try {
|
||||
couponData = await sdk.forConsole.billing.getCouponAccount(code);
|
||||
couponData = await sdk.forConsole.console.getCoupon({
|
||||
couponId: code
|
||||
});
|
||||
if (couponData.campaign) {
|
||||
campaign = await sdk.forConsole.billing.getCampaign(couponData.campaign);
|
||||
campaign = await sdk.forConsole.console.getCampaign({
|
||||
campaignId: couponData.campaign
|
||||
});
|
||||
}
|
||||
return { couponData, campaign };
|
||||
} catch (e) {
|
||||
redirect(303, base);
|
||||
redirect(303, resolve('/'));
|
||||
}
|
||||
}
|
||||
// Has campaign
|
||||
else if (url.searchParams.has('campaign')) {
|
||||
const campaignId = url.searchParams.get('campaign');
|
||||
let campaign: Campaign;
|
||||
let campaign: Models.Campaign;
|
||||
try {
|
||||
campaign = await sdk.forConsole.billing.getCampaign(campaignId);
|
||||
campaign = await sdk.forConsole.console.getCampaign({ campaignId });
|
||||
return { campaign };
|
||||
} catch (e) {
|
||||
redirect(303, base);
|
||||
redirect(303, resolve('/'));
|
||||
}
|
||||
}
|
||||
// No campaign or promo code
|
||||
else {
|
||||
redirect(303, base);
|
||||
redirect(303, resolve('/'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto, invalidate, preloadData } from '$app/navigation';
|
||||
import { base, resolve } from '$app/paths';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { PlanComparisonBox, PlanSelection, SelectPaymentMethod } from '$lib/components/billing';
|
||||
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputTags, InputText } from '$lib/elements/forms';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { isOrganization, tierToPlan } from '$lib/stores/billing';
|
||||
import {
|
||||
billingIdToPlan,
|
||||
getBasePlanFromGroup,
|
||||
isPaymentAuthenticationRequired
|
||||
} from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { OrganizationError, Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmPayment } from '$lib/stores/stripe';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { BillingPlanGroup, ID, type Models } from '@appwrite.io/console';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Divider, Fieldset, Icon, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -25,9 +27,9 @@
|
||||
const { data }: PageProps = $props();
|
||||
|
||||
let showExitModal = $state(false);
|
||||
let selectedPlan = $state(data.plan);
|
||||
let previousPage: string = $state(resolve('/(console)'));
|
||||
let selectedPlan: BillingPlan = $state(data.plan as BillingPlan);
|
||||
let selectedCoupon: Partial<Coupon> | null = $state(data.coupon);
|
||||
let selectedCoupon: Partial<Models.Coupon> | null = $state(data.coupon);
|
||||
|
||||
let isSubmitting = $state(writable(false));
|
||||
let formComponent: Form | null = $state(null);
|
||||
@@ -36,7 +38,6 @@
|
||||
let taxId: string | null = $state(null);
|
||||
let collaborators: string[] = $state([]);
|
||||
let paymentMethodId: string | null = $state(null);
|
||||
let billingPlan: BillingPlan = $state(BillingPlan.FREE);
|
||||
|
||||
let showCreditModal = $state(false);
|
||||
let billingBudget: number | undefined = $state(undefined);
|
||||
@@ -49,7 +50,9 @@
|
||||
if (page.url.searchParams.has('coupon')) {
|
||||
const coupon = page.url.searchParams.get('coupon');
|
||||
try {
|
||||
selectedCoupon = await sdk.forConsole.billing.getCouponAccount(coupon);
|
||||
selectedCoupon = await sdk.forConsole.console.getCoupon({
|
||||
couponId: coupon
|
||||
});
|
||||
} catch (e) {
|
||||
selectedCoupon = {
|
||||
code: null,
|
||||
@@ -61,18 +64,21 @@
|
||||
if (page.url.searchParams.has('name')) {
|
||||
name = page.url.searchParams.get('name');
|
||||
}
|
||||
|
||||
if (page.url.searchParams.has('plan')) {
|
||||
const plan = page.url.searchParams.get('plan');
|
||||
if (plan && Object.values(BillingPlan).includes(plan as BillingPlan)) {
|
||||
billingPlan = plan as BillingPlan;
|
||||
if (plan) {
|
||||
selectedPlan = billingIdToPlan(plan);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
data?.hasFreeOrganizations ||
|
||||
(page.url.searchParams.has('type') && page.url.searchParams.get('type') === 'createPro')
|
||||
) {
|
||||
billingPlan = BillingPlan.PRO;
|
||||
selectedPlan = getBasePlanFromGroup(BillingPlanGroup.Pro);
|
||||
}
|
||||
|
||||
if (page.url.searchParams.has('type')) {
|
||||
const type = page.url.searchParams.get('type');
|
||||
if (type === 'payment_confirmed') {
|
||||
@@ -83,12 +89,24 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function preloadAndNavigate(organizationId: string) {
|
||||
const resolvedUrl = resolve('/(console)/organization-[organization]', {
|
||||
organization: organizationId
|
||||
});
|
||||
|
||||
await preloadData(resolvedUrl);
|
||||
await goto(resolvedUrl);
|
||||
}
|
||||
|
||||
async function validate(organizationId: string, invites: string[]) {
|
||||
try {
|
||||
const org = await sdk.forConsole.billing.validateOrganization(organizationId, invites);
|
||||
if (isOrganization(org)) {
|
||||
await preloadData(`${base}/console/organization-${org.$id}`);
|
||||
await goto(`${base}/console/organization-${org.$id}`);
|
||||
const org = await sdk.forConsole.organizations.validatePayment({
|
||||
organizationId,
|
||||
invites
|
||||
});
|
||||
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
await preloadAndNavigate(org.$id);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${org.name ?? 'Organization'} has been created`
|
||||
@@ -105,59 +123,58 @@
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
let org: Organization | OrganizationError;
|
||||
let org: Models.Organization | Models.PaymentAuthentication;
|
||||
|
||||
if (selectedPlan === BillingPlan.FREE) {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
name,
|
||||
BillingPlan.FREE,
|
||||
null
|
||||
);
|
||||
if (selectedPlan.group === BillingPlanGroup.Starter) {
|
||||
org = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: name,
|
||||
billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id
|
||||
});
|
||||
} else {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
org = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name,
|
||||
selectedPlan,
|
||||
billingPlan: selectedPlan.$id,
|
||||
paymentMethodId,
|
||||
undefined,
|
||||
selectedCoupon?.code,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
couponId: selectedCoupon?.code,
|
||||
invites: collaborators,
|
||||
budget: billingBudget,
|
||||
taxId
|
||||
);
|
||||
});
|
||||
|
||||
if (!isOrganization(org) && org.status === 402) {
|
||||
if (isPaymentAuthenticationRequired(org)) {
|
||||
let clientSecret = org.clientSecret;
|
||||
let params = new URLSearchParams();
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('id', org.teamId);
|
||||
params.append('id', org.organizationId);
|
||||
for (const [key, value] of page.url.searchParams.entries()) {
|
||||
if (key !== 'type' && key !== 'id') {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
params.append('invites', collaborators.join(','));
|
||||
await confirmPayment(
|
||||
'',
|
||||
const resolvedUrl = resolve('/(console)/create-organization');
|
||||
|
||||
await confirmPayment({
|
||||
clientSecret,
|
||||
paymentMethodId,
|
||||
`${base}/create-organization?${params}`
|
||||
);
|
||||
await validate(org.teamId, collaborators);
|
||||
route: `${resolvedUrl}?${params}`
|
||||
});
|
||||
|
||||
await validate(org.organizationId, collaborators);
|
||||
}
|
||||
}
|
||||
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: tierToPlan(billingPlan)?.name,
|
||||
plan: selectedPlan.name,
|
||||
budget_cap_enabled: billingBudget !== null,
|
||||
members_invited: collaborators?.length
|
||||
});
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await preloadData(`${base}/organization-${org.$id}`);
|
||||
await goto(`${base}/organization-${org.$id}`);
|
||||
await preloadAndNavigate(org.$id);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${org.name ?? 'Organization'} has been created`
|
||||
@@ -202,11 +219,12 @@
|
||||
|
||||
<PlanSelection
|
||||
isNewOrg
|
||||
bind:billingPlan={selectedPlan}
|
||||
bind:selectedBillingPlan={selectedPlan}
|
||||
anyOrgFree={data.hasFreeOrganizations} />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
{#if selectedPlan !== BillingPlan.FREE}
|
||||
|
||||
{#if selectedPlan.supportsCredits}
|
||||
<Fieldset legend="Payment">
|
||||
<Layout.Stack gap="s" alignItems="flex-start">
|
||||
<SelectPaymentMethod
|
||||
@@ -243,7 +261,7 @@
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if selectedPlan !== BillingPlan.FREE}
|
||||
{#if selectedPlan.supportsCredits}
|
||||
<EstimatedTotalBox
|
||||
billingPlan={selectedPlan}
|
||||
{collaborators}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { BillingPlanGroup, type Models, Platform } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { billingIdToPlan, getBasePlanFromGroup, getNextTierBillingPlan } from '$lib/stores/billing';
|
||||
|
||||
export const load: PageLoad = async ({ url, parent, depends }) => {
|
||||
const { organizations } = await parent();
|
||||
depends(Dependencies.ORGANIZATIONS);
|
||||
|
||||
const [coupon, paymentMethods, plans] = await Promise.all([
|
||||
getCoupon(url),
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listPlans()
|
||||
sdk.forConsole.account.listPaymentMethods(),
|
||||
sdk.forConsole.console.getPlans({
|
||||
platform: Platform.Appwrite
|
||||
})
|
||||
]);
|
||||
let plan = getPlanFromUrl(url);
|
||||
const hasFreeOrganizations = organizations.teams?.some(
|
||||
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
|
||||
);
|
||||
|
||||
if (plan === BillingPlan.FREE && hasFreeOrganizations) {
|
||||
plan = BillingPlan.PRO;
|
||||
let plan = await getPlanFromUrl(url);
|
||||
const hasFreeOrganizations = organizations.teams?.some((org) => {
|
||||
const organization = org as Models.Organization;
|
||||
return organization.billingPlanDetails.group === BillingPlanGroup.Starter;
|
||||
});
|
||||
|
||||
if (plan?.group === BillingPlanGroup.Starter && hasFreeOrganizations) {
|
||||
plan = getNextTierBillingPlan(plan?.$id);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -31,21 +36,33 @@ export const load: PageLoad = async ({ url, parent, depends }) => {
|
||||
};
|
||||
};
|
||||
|
||||
function getPlanFromUrl(url: URL): BillingPlan | null {
|
||||
async function getPlanFromUrl(url: URL): Promise<Models.BillingPlan | null> {
|
||||
if (url.searchParams.has('plan')) {
|
||||
const plan = url.searchParams.get('plan');
|
||||
if (plan && plan in BillingPlan) {
|
||||
return plan as BillingPlan;
|
||||
}
|
||||
const planId = url.searchParams.get('plan');
|
||||
// check if available in cache, if not, fetch from API.
|
||||
return getPlanFromCache(planId) ?? (await sdk.forConsole.console.getPlan({ planId }));
|
||||
}
|
||||
return BillingPlan.FREE;
|
||||
|
||||
// fallback
|
||||
const baseStarter = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
return await sdk.forConsole.console.getPlan({ planId: baseStarter.$id });
|
||||
}
|
||||
|
||||
async function getCoupon(url: URL): Promise<Coupon | null> {
|
||||
function getPlanFromCache(plan: string): Models.BillingPlan | null {
|
||||
try {
|
||||
return billingIdToPlan(plan);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCoupon(url: URL): Promise<Models.Coupon | null> {
|
||||
if (url.searchParams.has('code')) {
|
||||
const coupon = url.searchParams.get('code');
|
||||
try {
|
||||
return sdk.forConsole.billing.getCouponAccount(coupon);
|
||||
return sdk.forConsole.account.getCoupon({
|
||||
couponId: coupon
|
||||
});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { isCloud } from '$lib/system';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { BillingPlanGroup, ID } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { loadAvailableRegions } from '$routes/(console)/regions';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Button, Card, Layout, Input, Typography, Spinner } from '@appwrite.io/pink-svelte';
|
||||
import { Form } from '$lib/elements/forms/index.js';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { resolve } from '$app/paths';
|
||||
import { getBasePlanFromGroup } from '$lib/stores/billing';
|
||||
|
||||
let isLoading = false;
|
||||
let organizationName = 'Personal Projects';
|
||||
let isLoading = $state(false);
|
||||
let organizationName = $state('Personal Projects');
|
||||
|
||||
async function createOrganization() {
|
||||
isLoading = true;
|
||||
@@ -21,15 +21,15 @@
|
||||
|
||||
try {
|
||||
if (isCloud) {
|
||||
organization = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
organizationName,
|
||||
BillingPlan.FREE,
|
||||
null
|
||||
);
|
||||
const starter = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
organization = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: organizationName,
|
||||
billingPlan: starter.$id
|
||||
});
|
||||
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: tierToPlan(BillingPlan.FREE)?.name,
|
||||
plan: starter?.name,
|
||||
budget_cap_enabled: false,
|
||||
members_invited: 0
|
||||
});
|
||||
@@ -48,7 +48,11 @@
|
||||
} finally {
|
||||
if (organization) {
|
||||
loadAvailableRegions(organization?.$id).then();
|
||||
await goto(`${base}/organization-${organization.$id}`);
|
||||
await goto(
|
||||
resolve('/(console)/organization-[organization]', {
|
||||
organization: organization.$id
|
||||
})
|
||||
);
|
||||
|
||||
// fixes an edge case where
|
||||
// the org is not available for some reason!
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { isOrganization, tierToPlan } from '$lib/stores/billing';
|
||||
import { ID, Query, type Models } from '@appwrite.io/console';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { getBasePlanFromGroup, isPaymentAuthenticationRequired } from '$lib/stores/billing';
|
||||
import { BillingPlanGroup, ID, type Models, Query } from '@appwrite.io/console';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
@@ -25,28 +24,28 @@ export const load: PageLoad = async ({ parent }) => {
|
||||
if (!organizations?.total) {
|
||||
try {
|
||||
if (isCloud) {
|
||||
const org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal projects',
|
||||
BillingPlan.FREE,
|
||||
null
|
||||
);
|
||||
const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
const org = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: 'Personal projects',
|
||||
billingPlan: starterPlan.$id
|
||||
});
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: tierToPlan(BillingPlan.FREE)?.name,
|
||||
plan: starterPlan?.name,
|
||||
budget_cap_enabled: false,
|
||||
members_invited: 0
|
||||
});
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
return {
|
||||
accountPrefs,
|
||||
organization: org
|
||||
};
|
||||
} else {
|
||||
const e = new Error(org.message, {
|
||||
const error = new Error(org.message, {
|
||||
cause: org
|
||||
});
|
||||
trackError(e, Submit.OrganizationCreate);
|
||||
trackError(error, Submit.OrganizationCreate);
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
|
||||
@@ -11,10 +11,8 @@ import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte
|
||||
import { get } from 'svelte/store';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { defaultRoles, defaultScopes } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { loadAvailableRegions } from '$routes/(console)/regions';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { Platform } from '@appwrite.io/console';
|
||||
import { type Models, Platform } from '@appwrite.io/console';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
@@ -28,26 +26,37 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
|
||||
let roles = isCloud ? [] : defaultRoles;
|
||||
let scopes = isCloud ? [] : defaultScopes;
|
||||
let currentPlan: Plan = null;
|
||||
let currentPlan: Models.BillingPlan | null = null;
|
||||
|
||||
try {
|
||||
if (isCloud) {
|
||||
[{ roles, scopes }, currentPlan] = await Promise.all([
|
||||
sdk.forConsole.billing.getRoles(params.organization),
|
||||
sdk.forConsole.billing.getOrganizationPlan(params.organization)
|
||||
sdk.forConsole.organizations.getScopes({
|
||||
organizationId: params.organization
|
||||
}),
|
||||
|
||||
sdk.forConsole.organizations.getPlan({
|
||||
organizationId: params.organization
|
||||
})
|
||||
]);
|
||||
|
||||
if (scopes.includes('billing.read')) {
|
||||
loadFailedInvoices(params.organization);
|
||||
}
|
||||
}
|
||||
|
||||
if (prefs.organization !== params.organization) {
|
||||
const newPrefs = { ...prefs, organization: params.organization };
|
||||
sdk.forConsole.account.updatePrefs({ prefs: newPrefs });
|
||||
}
|
||||
|
||||
const program: Models.Program | null =
|
||||
currentPlan && currentPlan?.program ? currentPlan.program : null;
|
||||
|
||||
const [org, members, countryList, locale] = await Promise.all([
|
||||
sdk.forConsole.teams.get({ teamId: params.organization }) as Promise<Organization>,
|
||||
sdk.forConsole.teams.get({
|
||||
teamId: params.organization
|
||||
}) as Promise<Models.Organization>,
|
||||
sdk.forConsole.teams.listMemberships({ teamId: params.organization }),
|
||||
sdk.forConsole.locale.listCountries(),
|
||||
sdk.forConsole.locale.get(),
|
||||
@@ -64,7 +73,8 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
|
||||
roles,
|
||||
scopes,
|
||||
countryList,
|
||||
locale
|
||||
locale,
|
||||
program
|
||||
};
|
||||
} catch (e) {
|
||||
const newPrefs = { ...prefs, organization: null };
|
||||
@@ -94,7 +104,7 @@ async function checkPlatformAndRedirect(
|
||||
) {
|
||||
// check if preloaded
|
||||
let requestedOrg = organizations.teams.find((team) => team.$id === params.organization) as
|
||||
| Organization
|
||||
| Models.Organization
|
||||
| undefined;
|
||||
|
||||
// not found, load!
|
||||
@@ -102,7 +112,7 @@ async function checkPlatformAndRedirect(
|
||||
try {
|
||||
requestedOrg = (await sdk.forConsole.teams.get({
|
||||
teamId: params.organization
|
||||
})) as Organization;
|
||||
})) as Models.Organization;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
@@ -133,7 +143,7 @@ async function checkPlatformAndRedirect(
|
||||
// check if exists and is valid
|
||||
const orgFromPrefs = (await sdk.forConsole.teams.get({
|
||||
teamId: orgIdInPrefs
|
||||
})) as Organization;
|
||||
})) as Models.Organization;
|
||||
|
||||
// exists and is valid, redirect
|
||||
redirect(
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
} from '$lib/components';
|
||||
import { trackEvent, Click } from '$lib/actions/analytics';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { getServiceLimit, readOnly, upgradeURL } from '$lib/stores/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { getServiceLimit, readOnly, getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
|
||||
import { onMount, type ComponentType } from 'svelte';
|
||||
import { canWriteProjects } from '$lib/stores/roles';
|
||||
@@ -38,7 +37,7 @@
|
||||
import type { PageProps } from './$types';
|
||||
import { getPlatformInfo } from '$lib/helpers/platform';
|
||||
import CreateProjectCloud from './createProjectCloud.svelte';
|
||||
import { currentPlan, regions as regionsStore } from '$lib/stores/organization';
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte';
|
||||
import ArchiveProject from '$lib/components/archiveProject.svelte';
|
||||
|
||||
@@ -197,7 +196,7 @@
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > $currentPlan.projects)}
|
||||
{#if isCloud && data.currentPlan?.projects && data.currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > data.currentPlan.projects)}
|
||||
{@const difference = projectsToArchive.length}
|
||||
{@const messagePrefix =
|
||||
difference !== 1 ? `${difference} projects are` : `${difference} project is`}
|
||||
@@ -207,7 +206,7 @@
|
||||
<Button
|
||||
compact
|
||||
size="s"
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl(data.organization.$id)}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
@@ -220,7 +219,7 @@
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
{#if isCloud && data.organization.billingPlan === BillingPlan.FREE && projectsToArchive.length === 0 && !freePlanAlertDismissed}
|
||||
{#if isCloud && data.currentPlan?.projects !== 0 && projectsToArchive.length === 0 && !freePlanAlertDismissed}
|
||||
<Alert.Inline dismissible on:dismiss={dismissFreePlanAlert}>
|
||||
<Typography.Text
|
||||
>Your Free plan includes up to 2 projects and limited resources. Upgrade to unlock
|
||||
@@ -229,7 +228,7 @@
|
||||
<Button
|
||||
compact
|
||||
size="s"
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl(data.organization.$id)}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
@@ -336,7 +335,7 @@
|
||||
<ArchiveProject
|
||||
{projectsToArchive}
|
||||
organization={data.organization}
|
||||
currentPlan={$currentPlan}
|
||||
currentPlan={data.currentPlan}
|
||||
archivedTotalOverall={data.archivedTotalOverall}
|
||||
archivedOffset={data.archivedOffset}
|
||||
limit={data.limit} />
|
||||
@@ -344,7 +343,8 @@
|
||||
<CreateOrganization bind:show={addOrganization} />
|
||||
<CreateProject bind:show={showCreate} teamId={page.params.organization} />
|
||||
<CreateProjectCloud
|
||||
projects={data.projects.total}
|
||||
bind:showCreateProjectCloud
|
||||
projects={data.projects.total}
|
||||
regions={$regionsStore.regions}
|
||||
teamId={page.params.organization} />
|
||||
teamId={page.params.organization}
|
||||
currentPlan={data.currentPlan} />
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
import AvailableCredit from './availableCredit.svelte';
|
||||
import PaymentHistory from './paymentHistory.svelte';
|
||||
import TaxId from './taxId.svelte';
|
||||
import { failedInvoice, tierToPlan, upgradeURL, useNewPricingModal } from '$lib/stores/billing';
|
||||
import {
|
||||
failedInvoice,
|
||||
billingIdToPlan,
|
||||
getChangePlanUrl,
|
||||
useNewPricingModal
|
||||
} from '$lib/stores/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { confirmPayment } from '$lib/stores/stripe';
|
||||
@@ -27,13 +32,13 @@
|
||||
|
||||
$: organization = data.organization;
|
||||
$: baseUrl = resolve('/(console)/organization-[organization]/billing', {
|
||||
organization: organization.$id
|
||||
organization: page.params.organization
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (page.url.searchParams.has('type')) {
|
||||
if (page.url.searchParams.get('type') === 'upgrade') {
|
||||
goto($upgradeURL);
|
||||
await goto(getChangePlanUrl(page.params.organization));
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -41,17 +46,17 @@
|
||||
page.url.searchParams.get('type') === 'confirmation'
|
||||
) {
|
||||
const invoiceId = page.url.searchParams.get('invoice');
|
||||
const invoice = await sdk.forConsole.billing.getInvoice(
|
||||
page.params.organization,
|
||||
const invoice = await sdk.forConsole.organizations.getInvoice({
|
||||
organizationId: page.params.organization,
|
||||
invoiceId
|
||||
);
|
||||
});
|
||||
|
||||
await confirmPayment(
|
||||
organization.$id,
|
||||
invoice.clientSecret,
|
||||
organization.paymentMethodId,
|
||||
`${baseUrl}?type=validate-invoice&invoice=${invoice.$id}`
|
||||
);
|
||||
await confirmPayment({
|
||||
clientSecret: invoice.clientSecret,
|
||||
paymentMethodId: organization.paymentMethodId,
|
||||
orgId: organization.$id,
|
||||
route: `${baseUrl}?type=validate-invoice&invoice=${invoice.$id}`
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -59,9 +64,14 @@
|
||||
page.url.searchParams.get('type') === 'validate-invoice'
|
||||
) {
|
||||
const invoiceId = page.url.searchParams.get('invoice');
|
||||
await sdk.forConsole.billing.updateInvoiceStatus(organization.$id, invoiceId);
|
||||
invalidate(Dependencies.INVOICES);
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
await sdk.forConsole.organizations.validateInvoice({
|
||||
organizationId: organization.$id,
|
||||
invoiceId
|
||||
});
|
||||
await Promise.all([
|
||||
invalidate(Dependencies.INVOICES),
|
||||
invalidate(Dependencies.ORGANIZATION)
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -69,17 +79,21 @@
|
||||
page.url.searchParams.get('type') === 'retry'
|
||||
) {
|
||||
const invoiceId = page.url.searchParams.get('invoice');
|
||||
const invoice = await sdk.forConsole.billing.getInvoice(
|
||||
page.params.organization,
|
||||
const invoice = await sdk.forConsole.organizations.getInvoice({
|
||||
organizationId: page.params.organization,
|
||||
invoiceId
|
||||
);
|
||||
});
|
||||
selectedInvoice.set(invoice);
|
||||
showRetryModal.set(true);
|
||||
}
|
||||
}
|
||||
if (page.url.searchParams.has('clientSecret')) {
|
||||
const clientSecret = page.url.searchParams.get('clientSecret');
|
||||
await confirmPayment(organization.$id, clientSecret, organization.paymentMethodId);
|
||||
await confirmPayment({
|
||||
clientSecret,
|
||||
paymentMethodId: organization.paymentMethodId,
|
||||
orgId: organization.$id
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -117,41 +131,43 @@
|
||||
{/if}
|
||||
{#if organization?.billingPlanDowngrade}
|
||||
<Alert.Inline status="info">
|
||||
Your organization has changed to {tierToPlan(organization?.billingPlanDowngrade).name} plan.
|
||||
You will continue to have access to {tierToPlan(organization?.billingPlan).name} plan features
|
||||
until your billing period ends on {toLocaleDate(organization.billingNextInvoiceDate)}.
|
||||
Your organization has changed to {billingIdToPlan(organization?.billingPlanDowngrade)
|
||||
.name} plan. You will continue to have access to {organization?.billingPlanDetails
|
||||
?.name} plan features until your billing period ends on {toLocaleDate(
|
||||
organization.billingNextInvoiceDate
|
||||
)}.
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
{#if $useNewPricingModal}
|
||||
<PlanSummary
|
||||
availableCredit={data?.availableCredit}
|
||||
currentPlan={data?.currentPlan}
|
||||
nextPlan={data?.nextPlan}
|
||||
currentAggregation={data?.billingAggregation}
|
||||
limit={data?.limit}
|
||||
offset={data?.offset} />
|
||||
availableCredit={data.availableCredit}
|
||||
currentPlan={data.currentPlan}
|
||||
nextPlan={data.nextPlan}
|
||||
currentAggregation={data.billingAggregation}
|
||||
limit={data.limit}
|
||||
offset={data.offset} />
|
||||
{:else}
|
||||
<PlanSummaryOld
|
||||
availableCredit={data?.availableCredit}
|
||||
currentPlan={data?.currentPlan}
|
||||
currentAggregation={data?.billingAggregation}
|
||||
currentInvoice={data?.billingInvoice} />
|
||||
availableCredit={data.availableCredit}
|
||||
currentPlan={data.currentPlan}
|
||||
currentAggregation={data.billingAggregation}
|
||||
currentInvoice={data.billingInvoice} />
|
||||
{/if}
|
||||
<PaymentHistory />
|
||||
|
||||
<PaymentMethods
|
||||
methods={data?.paymentMethods}
|
||||
organization={data?.organization}
|
||||
organization={data.organization}
|
||||
paymentMethods={data.paymentMethods}
|
||||
backupMethod={data.backupPaymentMethod}
|
||||
primaryMethod={data.primaryPaymentMethod} />
|
||||
|
||||
<BillingAddress
|
||||
organization={data?.organization}
|
||||
billingAddress={data?.billingAddress}
|
||||
locale={data?.locale}
|
||||
countryList={data?.countryList} />
|
||||
locale={data.locale}
|
||||
countryList={data.countryList}
|
||||
organization={data.organization}
|
||||
billingAddress={data.billingAddress} />
|
||||
<TaxId />
|
||||
<BudgetCap organization={data?.organization} currentPlan={data?.currentPlan} />
|
||||
<BudgetCap organization={data.organization} currentPlan={data.currentPlan} />
|
||||
<AvailableCredit areCreditsSupported={data.areCreditsSupported} />
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { BillingPlan, DEFAULT_BILLING_PROJECTS_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { Address, PaymentList } from '$lib/sdk/billing';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { resolve } from '$app/paths';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { base } from '$app/paths';
|
||||
import { type PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { DEFAULT_BILLING_PROJECTS_LIMIT, Dependencies } from '$lib/constants';
|
||||
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
|
||||
export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
const { organization, scopes, currentPlan, countryList, locale } = await parent();
|
||||
|
||||
if (!scopes.includes('billing.read')) {
|
||||
return redirect(301, `${base}/organization-${organization.$id}`);
|
||||
return redirect(
|
||||
302,
|
||||
resolve('/(console)/organization-[organization]', {
|
||||
organization: organization.$id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
@@ -25,10 +28,13 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
// aggregation reloads on page param changes
|
||||
depends(Dependencies.BILLING_AGGREGATION);
|
||||
|
||||
const billingAddressId = (organization as Organization)?.billingAddressId;
|
||||
const billingAddressPromise: Promise<Address> = billingAddressId
|
||||
? sdk.forConsole.billing
|
||||
.getOrganizationBillingAddress(organization.$id, billingAddressId)
|
||||
const billingAddressId = (organization as Models.Organization)?.billingAddressId;
|
||||
const billingAddressPromise: Promise<Models.BillingAddress> = billingAddressId
|
||||
? sdk.forConsole.organizations
|
||||
.getBillingAddress({
|
||||
organizationId: organization.$id,
|
||||
billingAddressId
|
||||
})
|
||||
.catch(() => null)
|
||||
: null;
|
||||
|
||||
@@ -37,47 +43,47 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
* initially created, these might return 404
|
||||
* - can be removed later once that is fixed in back-end
|
||||
*/
|
||||
let billingAggregation = null;
|
||||
let billingAggregation: Models.AggregationTeam | null = null;
|
||||
try {
|
||||
const currentPage = getPage(url) || 1;
|
||||
const limit = getLimit(url, route, DEFAULT_BILLING_PROJECTS_LIMIT);
|
||||
const offset = pageToOffset(currentPage, limit);
|
||||
billingAggregation = await sdk.forConsole.billing.getAggregation(
|
||||
organization.$id,
|
||||
(organization as Organization)?.billingAggregationId,
|
||||
billingAggregation = await sdk.forConsole.organizations.getAggregation({
|
||||
organizationId: organization.$id,
|
||||
aggregationId: (organization as Models.Organization)?.billingAggregationId,
|
||||
limit,
|
||||
offset
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
let billingInvoice = null;
|
||||
try {
|
||||
billingInvoice = await sdk.forConsole.billing.getInvoice(
|
||||
organization.$id,
|
||||
(organization as Organization)?.billingInvoiceId
|
||||
);
|
||||
billingInvoice = await sdk.forConsole.organizations.getInvoice({
|
||||
organizationId: organization.$id,
|
||||
invoiceId: (organization as Models.Organization)?.billingInvoiceId
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
const areCreditsSupported = isCloud
|
||||
? (currentPlan?.supportsCredits ??
|
||||
(organization.billingPlan !== BillingPlan.FREE &&
|
||||
organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION))
|
||||
: false;
|
||||
const areCreditsSupported = isCloud ? currentPlan?.supportsCredits : false;
|
||||
|
||||
const [paymentMethods, addressList, billingAddress, availableCredit, billingPlanDowngrade] =
|
||||
await Promise.all([
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
sdk.forConsole.account.listPaymentMethods(),
|
||||
sdk.forConsole.account.listBillingAddresses(),
|
||||
billingAddressPromise,
|
||||
areCreditsSupported
|
||||
? sdk.forConsole.billing.getAvailableCredit(organization.$id)
|
||||
? sdk.forConsole.organizations.getAvailableCredits({
|
||||
organizationId: organization.$id
|
||||
})
|
||||
: null,
|
||||
organization.billingPlanDowngrade
|
||||
? sdk.forConsole.billing.getPlan(organization.billingPlanDowngrade)
|
||||
? sdk.forConsole.console.getPlan({
|
||||
planId: organization.billingPlanDowngrade
|
||||
})
|
||||
: null
|
||||
]);
|
||||
|
||||
@@ -108,14 +114,14 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
};
|
||||
|
||||
function getOrganizationPaymentMethods(
|
||||
organization: Organization,
|
||||
paymentMethods: PaymentList
|
||||
organization: Models.Organization,
|
||||
paymentMethods: Models.PaymentMethodList
|
||||
): {
|
||||
backup: PaymentMethodData | null;
|
||||
primary: PaymentMethodData | null;
|
||||
backup: Models.PaymentMethod | null;
|
||||
primary: Models.PaymentMethod | null;
|
||||
} {
|
||||
let backup: PaymentMethodData | null = null;
|
||||
let primary: PaymentMethodData | null = null;
|
||||
let backup: Models.PaymentMethod | null = null;
|
||||
let primary: Models.PaymentMethod | null = null;
|
||||
|
||||
for (const paymentMethod of paymentMethods.paymentMethods) {
|
||||
if (paymentMethod.$id === organization.paymentMethodId) {
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
async function redeem() {
|
||||
try {
|
||||
await sdk.forConsole.billing.addCredit($organization.$id, coupon);
|
||||
await sdk.forConsole.organizations.addCredit({
|
||||
organizationId: $organization.$id,
|
||||
couponId: coupon
|
||||
});
|
||||
show = false;
|
||||
await invalidate(Dependencies.CREDIT);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
|
||||
@@ -18,12 +18,15 @@
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
await sdk.forConsole.billing.setOrganizationPaymentMethod(
|
||||
$organization.$id,
|
||||
$addCreditWizardStore.paymentMethodId
|
||||
);
|
||||
await sdk.forConsole.organizations.setDefaultPaymentMethod({
|
||||
organizationId: $organization.$id,
|
||||
paymentMethodId: $addCreditWizardStore.paymentMethodId
|
||||
});
|
||||
|
||||
await sdk.forConsole.billing.addCredit($organization.$id, $addCreditWizardStore.coupon);
|
||||
await sdk.forConsole.organizations.addCredit({
|
||||
organizationId: $organization.$id,
|
||||
couponId: $addCreditWizardStore.coupon
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Credit has been added to ${$organization.name}`
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Empty, PaginationInline } from '$lib/components';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import type { CreditList } from '$lib/sdk/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import AddCreditModal from './addCreditModal.svelte';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { getChangePlanUrl } from '$lib/stores/billing';
|
||||
|
||||
import { Alert, Badge, Icon, Link, Table, Tooltip, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
@@ -20,14 +18,16 @@
|
||||
export let areCreditsSupported: boolean;
|
||||
|
||||
let offset = 0;
|
||||
let creditList: CreditList = {
|
||||
available: 0,
|
||||
credits: [],
|
||||
total: 0
|
||||
};
|
||||
let show = false;
|
||||
let reloadOnWizardClose = false;
|
||||
|
||||
/* default credits until loaded */
|
||||
let creditList: Models.CreditList = {
|
||||
total: 0,
|
||||
credits: [],
|
||||
available: 0
|
||||
};
|
||||
|
||||
onMount(request);
|
||||
|
||||
const limit = 5;
|
||||
@@ -44,10 +44,10 @@
|
||||
* Technically, we could reuse that for offsets < 25 (i.e., the first 5 pages with limit = 5)
|
||||
* to avoid an extra request. But for now, we always fetch fresh data.
|
||||
*/
|
||||
creditList = await sdk.forConsole.billing.listCredits($organization.$id, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset)
|
||||
]);
|
||||
creditList = await sdk.forConsole.organizations.listCredits({
|
||||
organizationId: $organization.$id,
|
||||
queries: [Query.limit(limit), Query.offset(offset)]
|
||||
});
|
||||
|
||||
creditList = {
|
||||
...creditList,
|
||||
@@ -81,7 +81,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid hideFooter={$organization?.billingPlan !== BillingPlan.FREE}>
|
||||
<CardGrid hideFooter={areCreditsSupported}>
|
||||
<svelte:fragment slot="title">
|
||||
{!areCreditsSupported ? 'Credits' : 'Available credit'}
|
||||
</svelte:fragment>
|
||||
@@ -171,10 +171,10 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if !areCreditsSupported}
|
||||
<Button
|
||||
secondary
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl($organization.$id)}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
import { CardGrid } from '$lib/components';
|
||||
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 { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import RemoveAddress from './removeAddress.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
@@ -24,10 +22,10 @@
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let organization: Organization;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
export let billingAddress: Address;
|
||||
export let organization: Models.Organization;
|
||||
export let billingAddress: Models.BillingAddress;
|
||||
|
||||
let showCreate = false;
|
||||
let showEdit = false;
|
||||
@@ -36,7 +34,10 @@
|
||||
|
||||
async function addAddress(addressId: string) {
|
||||
try {
|
||||
await sdk.forConsole.billing.setBillingAddress(organization.$id, addressId);
|
||||
await sdk.forConsole.organizations.setBillingAddress({
|
||||
organizationId: organization.$id,
|
||||
billingAddressId: addressId
|
||||
});
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { getChangePlanUrl } from '$lib/stores/billing';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Alert, Icon, Table } from '@appwrite.io/pink-svelte';
|
||||
import { IconTrash } from '@appwrite.io/pink-icons-svelte';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let organization: Organization;
|
||||
export let currentPlan: Plan;
|
||||
export let alertsEnabled = false;
|
||||
export let currentPlan: Models.BillingPlan;
|
||||
export let organization: Models.Organization;
|
||||
|
||||
let search: string;
|
||||
let selectedAlert: number;
|
||||
@@ -47,19 +46,20 @@
|
||||
|
||||
async function updateBudget() {
|
||||
try {
|
||||
await sdk.forConsole.billing.updateBudget(
|
||||
organization.$id,
|
||||
organization.billingBudget,
|
||||
await sdk.forConsole.organizations.updateBudget({
|
||||
organizationId: organization.$id,
|
||||
budget: organization.billingBudget,
|
||||
alerts
|
||||
);
|
||||
});
|
||||
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
isHtml: true,
|
||||
message: `<span> ${alerts.length === 0 ? 'Budget alerts removed from' : alerts.length > 1 ? `Budget alerts added to` : 'A budget alert has been added to'} <b>${organization.name}</b> </span>`
|
||||
});
|
||||
|
||||
trackEvent(Submit.BudgetAlertsUpdate, {
|
||||
alerts
|
||||
});
|
||||
@@ -80,8 +80,8 @@
|
||||
<svelte:fragment slot="title">Billing alerts</svelte:fragment>
|
||||
{#if !currentPlan.budgeting}
|
||||
Get notified by email when your organization meets a percentage of your budget cap. <b
|
||||
>{tierToPlan(organization.billingPlan).name} organizations will receive one notification
|
||||
at 75% resource usage.</b>
|
||||
>{organization.billingPlanDetails.name} organizations will receive one notification at 75%
|
||||
resource usage.</b>
|
||||
{:else}
|
||||
Get notified by email when your organization meets or exceeds a percentage of your specified
|
||||
billing alert(s).
|
||||
@@ -148,10 +148,10 @@
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Form onSubmit={updateBudget}>
|
||||
{#if organization?.billingPlan === BillingPlan.FREE || organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
|
||||
{#if !currentPlan.budgeting}
|
||||
<Button
|
||||
secondary
|
||||
href={$upgradeURL}
|
||||
href={getChangePlanUrl(organization.$id)}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user