diff --git a/e2e/steps/free-project.ts b/e2e/steps/free-project.ts index cf0972e478..b75791f103 100644 --- a/e2e/steps/free-project.ts +++ b/e2e/steps/free-project.ts @@ -24,9 +24,15 @@ export async function createFreeProject(page: Page): Promise { const regionPicker = dialog.locator('button[role="combobox"]'); if (await regionPicker.isVisible()) { await regionPicker.click(); - await page.getByRole('option', { name: /New York/i }).click(); - - region = 'nyc'; + const firstEnabledOption = page + .locator('[role="option"]:not([data-disabled="true"])') + .first(); + + 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(); diff --git a/e2e/steps/pro-project.ts b/e2e/steps/pro-project.ts index 9ac5f386d3..53208c2953 100644 --- a/e2e/steps/pro-project.ts +++ b/e2e/steps/pro-project.ts @@ -57,9 +57,15 @@ export async function createProProject(page: Page): Promise { 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(); diff --git a/src/lib/commandCenter/searchers/organizations.ts b/src/lib/commandCenter/searchers/organizations.ts index c24f38799e..9df8003a17 100644 --- a/src/lib/commandCenter/searchers/organizations.ts +++ b/src/lib/commandCenter/searchers/organizations.ts @@ -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; diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte index 8eb3795914..8a324b395b 100644 --- a/src/lib/components/archiveProject.svelte +++ b/src/lib/components/archiveProject.svelte @@ -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 diff --git a/src/lib/components/backupDatabaseAlert.svelte b/src/lib/components/backupDatabaseAlert.svelte index 1c2ceed16f..cc7e2f3263 100644 --- a/src/lib/components/backupDatabaseAlert.svelte +++ b/src/lib/components/backupDatabaseAlert.svelte @@ -1,11 +1,10 @@ {#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`} {subtitle} @@ -35,7 +36,7 @@ href={ctaURL} secondary fullWidthMobile - event={isFreePlan ? 'backup_banner_upgrade' : 'backup_banner_add'}> + event={!areBackupsAvailable ? 'backup_banner_upgrade' : 'backup_banner_add'}> {ctaText} diff --git a/src/lib/components/backupRestoreBox.svelte b/src/lib/components/backupRestoreBox.svelte index 8c4ac61af9..b97c4d5e3d 100644 --- a/src/lib/components/backupRestoreBox.svelte +++ b/src/lib/components/backupRestoreBox.svelte @@ -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; diff --git a/src/lib/components/billing/alerts/limitReached.svelte b/src/lib/components/billing/alerts/limitReached.svelte index 4885db75e1..d0944f801d 100644 --- a/src/lib/components/billing/alerts/limitReached.svelte +++ b/src/lib/components/billing/alerts/limitReached.svelte @@ -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'; -{#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)} + title={`${$organization.name} usage has reached the ${$organization.billingPlanDetails.name} plan limit`}> - Usage for the {$organization.name} organization has reached the limits of the {tierToPlan( - $organization.billingPlan - ).name} + Usage for the {$organization.name} organization has reached the limits of the {$organization + .billingPlanDetails.name} plan. Consider upgrading to increase your resource usage. @@ -29,7 +27,7 @@ {/if} - + {/if} diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte index dd78788903..ba4d01acb7 100644 --- a/src/lib/components/billing/estimatedTotalBox.svelte +++ b/src/lib/components/billing/estimatedTotalBox.svelte @@ -1,24 +1,22 @@ {#if estimation} @@ -105,6 +103,7 @@ {#if couponData?.status === 'active'} {/if} + Total due diff --git a/src/lib/components/billing/index.ts b/src/lib/components/billing/index.ts index 11e9245b3f..eac388917f 100644 --- a/src/lib/components/billing/index.ts +++ b/src/lib/components/billing/index.ts @@ -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'; diff --git a/src/lib/components/billing/paymentBoxes.svelte b/src/lib/components/billing/paymentBoxes.svelte index 27719cba54..5883b4460a 100644 --- a/src/lib/components/billing/paymentBoxes.svelte +++ b/src/lib/components/billing/paymentBoxes.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; export let group: string; export let name: string; export let defaultMethod: string = null; diff --git a/src/lib/components/billing/paymentModal.svelte b/src/lib/components/billing/paymentModal.svelte index e881cd93ef..bdda244fd3 100644 --- a/src/lib/components/billing/paymentModal.svelte +++ b/src/lib/components/billing/paymentModal.svelte @@ -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' diff --git a/src/lib/components/billing/planComparisonBox.svelte b/src/lib/components/billing/planComparisonBox.svelte index 4379cc6f88..7a71d2f01e 100644 --- a/src/lib/components/billing/planComparisonBox.svelte +++ b/src/lib/components/billing/planComparisonBox.svelte @@ -1,103 +1,130 @@ - {#each visibleTiers as tier} + {#each uniquePlans as plan} (selectedTab = tier)}> - {tierToPlan(tier).name} + active={selectedTab === plan.$id} + on:click={() => (selectedTab = plan.$id)}> + {plan.name} {/each} - {plan.name} plan - {#if selectedTab === BillingPlan.FREE} - {#if downgrade} -
    -
  • - - - Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions} - Functions per project - -
  • -
  • - - Limited to 1 organization member -
  • -
  • - - - {plan.bandwidth}GB bandwidth - -
  • -
  • - - - {plan.storage}GB storage - -
  • -
  • - - - {formatNum(plan.executions)} executions - -
  • -
- {:else} -
    -
  • - Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions} - Functions per project -
  • -
  • Limited to 1 organization member
  • -
  • - Limited to {plan.bandwidth}GB bandwidth -
  • -
  • - Limited to {plan.storage}GB storage -
  • -
  • - Limited to {formatNum(plan.executions)} executions -
  • -
- {/if} - {:else if selectedTab === BillingPlan.PRO} - Everything in the Free plan, plus: -
    -
  • Unlimited databases, buckets, functions
  • -
  • Unlimited seats
  • -
  • {plan.bandwidth}GB bandwidth
  • -
  • {plan.storage}GB storage
  • -
  • {formatNum(plan.executions)} executions
  • -
  • Email support
  • + {currentPlan.name} plan + + {@render appwritePlanView()} + + + +{#snippet appwritePlanView()} + {#if planHasGroup(selectedTab, BillingPlanGroup.Starter)} + {#if downgrade} +
      +
    • + + + Limited to {currentPlan.databases} + {pluralize(currentPlan.databases, 'Database')}, {currentPlan.buckets} + {pluralize(currentPlan.buckets, 'Bucket')}, {currentPlan.functions} + {pluralize(currentPlan.functions, 'Function')} per project + +
    • +
    • + + Limited to 1 organization member +
    • +
    • + + + {currentPlan.bandwidth}GB bandwidth + +
    • +
    • + + + {currentPlan.storage}GB storage + +
    • +
    • + + + {formatNum(currentPlan.executions)} executions + +
    - {:else if selectedTab === BillingPlan.SCALE} - Everything in the Pro plan, plus: + {:else}
      -
    • Unlimited seats
    • -
    • Organization roles
    • -
    • SOC-2, HIPAA compliance
    • -
    • SSO Coming soon
    • -
    • Priority support
    • +
    • + Limited to {currentPlan.databases} + {pluralize(currentPlan.databases, 'Database')}, {currentPlan.buckets} + {pluralize(currentPlan.buckets, 'Bucket')}, {currentPlan.functions} + {pluralize(currentPlan.functions, 'Function')} per project +
    • +
    • Limited to 1 organization member
    • +
    • + Limited to {currentPlan.bandwidth}GB bandwidth +
    • +
    • + Limited to {currentPlan.storage}GB storage +
    • +
    • + Limited to {formatNum(currentPlan.executions)} executions +
    {/if} - - + {:else if planHasGroup(selectedTab, BillingPlanGroup.Pro)} + Everything in the Free plan, plus: +
      +
    • Unlimited databases, buckets, functions
    • +
    • Unlimited seats
    • +
    • {currentPlan.bandwidth}GB bandwidth
    • +
    • {currentPlan.storage}GB storage
    • +
    • {formatNum(currentPlan.executions)} executions
    • +
    • Email support
    • +
    + {:else if planHasGroup(selectedTab, BillingPlanGroup.Scale)} + Everything in the Pro plan, plus: +
      +
    • Unlimited seats
    • +
    • Organization roles
    • +
    • SOC-2, HIPAA compliance
    • +
    • SSO Coming soon
    • +
    • Priority support
    • +
    + {/if} +{/snippet} diff --git a/src/lib/components/billing/planExcess.svelte b/src/lib/components/billing/planExcess.svelte deleted file mode 100644 index 7bdf28e3f8..0000000000 --- a/src/lib/components/billing/planExcess.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - -{#if showExcess} - - You will retain access to {tierToPlan($organization.billingPlan).name} plan features until your - billing period ends. After that, - {#if excess?.members > 0} - all team members except the owner will be removed, - {:else} - your organization will be limited to Free plan resources, - {/if} and service disruptions may occur if usage exceeds Free plan limits. - - - - - Resource - Free limit - - Excess usage - Metrics are estimates updated every 24 hours - - - - {#if excess?.members} - - Organization members - {getServiceLimit('members', tier)} members - -

    - - {excess?.members} members -

    -
    -
    - {/if} - {#if excess?.storage} - - Storage - {plan.storage} GB - -

    - - {humanFileSize(excess?.storage).value} - {humanFileSize(excess?.storage).unit} -

    -
    -
    - {/if} - {#if excess?.executions} - - Function executions - - {abbreviateNumber(plan.executions)} executions - - -

    - - - {formatNum(excess?.executions)} executions - -

    -
    -
    - {/if} - {#if excess?.users} - - Users - - {abbreviateNumber(plan.users)} users - - -

    - - - {formatNum(excess?.users)} users - -

    -
    -
    - {/if} -
    -{/if} diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index 6499a47c0b..d625de830c 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -1,41 +1,55 @@ - {#each plansWithoutScale as plan} + {#each visiblePlans as plan} - {#if $organization?.billingPlan === plan.$id && !isNewOrg} + {#if $organization?.billingPlanId === plan.$id && !isNewOrg} {/if} @@ -57,11 +71,11 @@ {#if $currentPlan && !currentPlanInList} - {#if $organization?.billingPlan === $currentPlan.$id && !isNewOrg} + {#if $organization?.billingPlanId === $currentPlan.$id && !isNewOrg} {/if} diff --git a/src/lib/components/billing/selectPaymentMethod.svelte b/src/lib/components/billing/selectPaymentMethod.svelte index c80aacacfe..9682b99953 100644 --- a/src/lib/components/billing/selectPaymentMethod.svelte +++ b/src/lib/components/billing/selectPaymentMethod.svelte @@ -1,6 +1,5 @@ - -{#if billingPlan} -
      - {#each $plansInfo.values() as plan} -
    • - - -
      -

      - {plan.name} - {#if $organization?.billingPlan === plan.$id && !isNewOrg} - Current plan - {/if} -

      -

      - {plan.desc} -

      -

      - {formatCurrency(plan?.price ?? 0)} -

      -
      -
      -
      -
    • - {/each} -
    -{/if} diff --git a/src/lib/components/billing/updateStateModal.svelte b/src/lib/components/billing/updateStateModal.svelte index 7887bc1aeb..db6160a7a0 100644 --- a/src/lib/components/billing/updateStateModal.svelte +++ b/src/lib/components/billing/updateStateModal.svelte @@ -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 @@ diff --git a/src/lib/components/billing/usageRates.svelte b/src/lib/components/billing/usageRates.svelte index 49ab414e93..0db4ac04e7 100644 --- a/src/lib/components/billing/usageRates.svelte +++ b/src/lib/components/billing/usageRates.svelte @@ -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; } {#if isFree} - 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)}. - {:else if org.billingPlan === BillingPlan.PRO} + {:else if org.billingPlanDetails.group === BillingPlanGroup.Pro} Usage on the Pro plan will be charged at the end of each billing period at the following rates. Next billing period: {toLocaleDate(nextDate)}. - {:else if org.billingPlan === BillingPlan.SCALE} + {:else if org.billingPlanDetails.group === BillingPlanGroup.Scale} 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 @@ Limit Rate - {#each Object.values(plan.addons) as addon} + {#each Object.values(org.billingPlanDetails.addons) as addon} {addon.invoiceDesc} @@ -69,7 +65,7 @@ {/if} {/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} diff --git a/src/lib/components/billing/validateCreditModal.svelte b/src/lib/components/billing/validateCreditModal.svelte index 9b2f026916..6e41448354 100644 --- a/src/lib/components/billing/validateCreditModal.svelte +++ b/src/lib/components/billing/validateCreditModal.svelte @@ -1,26 +1,28 @@ {#if !isOnOnboarding && filteredModalAlerts.length > 0 && currentModalAlert} - {@const shouldShowUpgrade = showUpgrade()} + {@const shouldShowUpgrade = canUpgrade($organization?.billingPlanDetails)}
    diff --git a/src/lib/components/breadcrumbs.svelte b/src/lib/components/breadcrumbs.svelte index 7f1a59c47c..16b5dc6d74 100644 --- a/src/lib/components/breadcrumbs.svelte +++ b/src/lib/components/breadcrumbs.svelte @@ -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; diff --git a/src/lib/components/cardContainer.svelte b/src/lib/components/cardContainer.svelte index a34931d4f1..27e444687d 100644 --- a/src/lib/components/cardContainer.svelte +++ b/src/lib/components/cardContainer.svelte @@ -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; diff --git a/src/lib/components/cardPlanLimit.svelte b/src/lib/components/cardPlanLimit.svelte index 3baae619de..8da63d7e8f 100644 --- a/src/lib/components/cardPlanLimit.svelte +++ b/src/lib/components/cardPlanLimit.svelte @@ -1,9 +1,19 @@
    @@ -11,7 +21,7 @@

    Upgrade your plan to add more {service}

    diff --git a/src/lib/components/creditCardInfo.svelte b/src/lib/components/creditCardInfo.svelte index f750e87c32..871a9b98ea 100644 --- a/src/lib/components/creditCardInfo.svelte +++ b/src/lib/components/creditCardInfo.svelte @@ -1,12 +1,12 @@ diff --git a/src/lib/components/emptyCardImageCloud.svelte b/src/lib/components/emptyCardImageCloud.svelte index 0f4c234504..14a03283ee 100644 --- a/src/lib/components/emptyCardImageCloud.svelte +++ b/src/lib/components/emptyCardImageCloud.svelte @@ -1,8 +1,8 @@ @@ -24,7 +26,7 @@ - + diff --git a/src/lib/components/feedback/feedback.svelte b/src/lib/components/feedback/feedback.svelte index ed500aa157..1b151c5d99 100644 --- a/src/lib/components/feedback/feedback.svelte +++ b/src/lib/components/feedback/feedback.svelte @@ -25,7 +25,7 @@ page.url.href, $user.name, $user.email, - $organization?.billingPlan, + $organization?.billingPlanId, $feedbackData.value, $organization?.$id, $project?.$id, diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index dac1cbb3d6..fc97c9b257 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -1,7 +1,6 @@ @@ -11,7 +10,7 @@ {#if isCloud} - {#if $organization?.billingPlan !== BillingPlan.FREE} + {#if $organization?.billingPlanDetails.supportsOrganizationRoles} Roles Owner, Developer, Editor, Analyst and Billing. @@ -39,7 +38,8 @@ text external href="https://appwrite.io/docs/advanced/platform/roles">Learn more - +

    {/if} {:else} @@ -59,7 +59,8 @@ text external href="https://appwrite.io/docs/advanced/platform/roles">Learn more - +

    {/if}
    diff --git a/src/lib/components/support.svelte b/src/lib/components/support.svelte index d05d2639c3..ef80269c5d 100644 --- a/src/lib/components/support.svelte +++ b/src/lib/components/support.svelte @@ -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`; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 33c900742a..2cb52605e3 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -609,17 +609,6 @@ export const eventServices: Array = [ } ]; -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', diff --git a/src/lib/elements/table/body.svelte b/src/lib/elements/table/body.svelte deleted file mode 100644 index a3ac5fe785..0000000000 --- a/src/lib/elements/table/body.svelte +++ /dev/null @@ -1,52 +0,0 @@ - - -
    - -
    -{#if isCloud && limitReached && service} - - - - - Upgrade your plan to add {name} to your organization - - - - - -{/if} diff --git a/src/lib/elements/table/cell.svelte b/src/lib/elements/table/cell.svelte deleted file mode 100644 index a35998e690..0000000000 --- a/src/lib/elements/table/cell.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -
    - -
    diff --git a/src/lib/elements/table/cellAvatar.svelte b/src/lib/elements/table/cellAvatar.svelte deleted file mode 100644 index 3d131be3fc..0000000000 --- a/src/lib/elements/table/cellAvatar.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - -
    - -
    -
    diff --git a/src/lib/elements/table/cellButton.svelte b/src/lib/elements/table/cellButton.svelte deleted file mode 100644 index b6a2277e02..0000000000 --- a/src/lib/elements/table/cellButton.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/src/lib/elements/table/cellCheck.svelte b/src/lib/elements/table/cellCheck.svelte deleted file mode 100644 index db1c8ab9c3..0000000000 --- a/src/lib/elements/table/cellCheck.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - - diff --git a/src/lib/elements/table/cellHead.svelte b/src/lib/elements/table/cellHead.svelte deleted file mode 100644 index d8bb55ca48..0000000000 --- a/src/lib/elements/table/cellHead.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
    - - - -
    diff --git a/src/lib/elements/table/cellHeadCheck.svelte b/src/lib/elements/table/cellHeadCheck.svelte deleted file mode 100644 index abff61e0fa..0000000000 --- a/src/lib/elements/table/cellHeadCheck.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/src/lib/elements/table/cellLink.svelte b/src/lib/elements/table/cellLink.svelte deleted file mode 100644 index 83ee17483c..0000000000 --- a/src/lib/elements/table/cellLink.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
    - -
    diff --git a/src/lib/elements/table/cellText.svelte b/src/lib/elements/table/cellText.svelte deleted file mode 100644 index 9bcd2262bc..0000000000 --- a/src/lib/elements/table/cellText.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/src/lib/elements/table/footer.svelte b/src/lib/elements/table/footer.svelte deleted file mode 100644 index d8f058c6b3..0000000000 --- a/src/lib/elements/table/footer.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/lib/elements/table/header.svelte b/src/lib/elements/table/header.svelte deleted file mode 100644 index bad3f122b4..0000000000 --- a/src/lib/elements/table/header.svelte +++ /dev/null @@ -1,5 +0,0 @@ -
    -
    - -
    -
    diff --git a/src/lib/elements/table/index.ts b/src/lib/elements/table/index.ts index 50bda1812d..b347931b7f 100644 --- a/src/lib/elements/table/index.ts +++ b/src/lib/elements/table/index.ts @@ -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'; diff --git a/src/lib/elements/table/row.svelte b/src/lib/elements/table/row.svelte deleted file mode 100644 index 3a9808d967..0000000000 --- a/src/lib/elements/table/row.svelte +++ /dev/null @@ -1,3 +0,0 @@ -
    - -
    diff --git a/src/lib/elements/table/rowButton.svelte b/src/lib/elements/table/rowButton.svelte deleted file mode 100644 index 9667a2ade3..0000000000 --- a/src/lib/elements/table/rowButton.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/src/lib/elements/table/rowLink.svelte b/src/lib/elements/table/rowLink.svelte deleted file mode 100644 index 7a8249bc77..0000000000 --- a/src/lib/elements/table/rowLink.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/src/lib/elements/table/table.svelte b/src/lib/elements/table/table.svelte deleted file mode 100644 index 179d411ce1..0000000000 --- a/src/lib/elements/table/table.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/src/lib/elements/table/tableList.svelte b/src/lib/elements/table/tableList.svelte deleted file mode 100644 index 7e2b4fe4b1..0000000000 --- a/src/lib/elements/table/tableList.svelte +++ /dev/null @@ -1,9 +0,0 @@ -
    -
    -
    -
      - -
    -
    -
    -
    diff --git a/src/lib/elements/table/tableScroll.svelte b/src/lib/elements/table/tableScroll.svelte index 91a7c80181..5c5b458bf0 100644 --- a/src/lib/elements/table/tableScroll.svelte +++ b/src/lib/elements/table/tableScroll.svelte @@ -12,7 +12,7 @@ let isOverflowing = false; - const hasOverflow: Action = (node) => { + const hasOverflow: Action = (node: Element) => { const observer = new ResizeObserver((entries) => { for (const entry of entries) { let overflowing = false; diff --git a/src/lib/flags.ts b/src/lib/flags.ts index dfbedecd9e..288091e791 100644 --- a/src/lib/flags.ts +++ b/src/lib/flags.ts @@ -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 (data: T) => { + return (data: T) => { const { account, organization } = data; return !!( diff --git a/src/lib/helpers/billing.ts b/src/lib/helpers/billing.ts new file mode 100644 index 0000000000..de3c2f538e --- /dev/null +++ b/src/lib/helpers/billing.ts @@ -0,0 +1,16 @@ +import type { Models } from '@appwrite.io/console'; + +export function makePlansMap( + plansArray: Models.BillingPlanList | null +): Map { + const plansMap = new Map(); + 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; +} diff --git a/src/lib/helpers/faker.ts b/src/lib/helpers/faker.ts index e05cbc85d3..c7ce16e9dd 100644 --- a/src/lib/helpers/faker.ts +++ b/src/lib/helpers/faker.ts @@ -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 { - const client = sdk.forProject(project.region, project.$id); + const client = sdk.forProject(project.region, project.id); switch (databaseType) { case 'legacy': diff --git a/src/lib/helpers/program.ts b/src/lib/helpers/program.ts new file mode 100644 index 0000000000..ead1fa9ecf --- /dev/null +++ b/src/lib/helpers/program.ts @@ -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 +}; diff --git a/src/lib/layout/containerButton.svelte b/src/lib/layout/containerButton.svelte index 53cfc35bf1..c331c2eb74 100644 --- a/src/lib/layout/containerButton.svelte +++ b/src/lib/layout/containerButton.svelte @@ -1,17 +1,17 @@ @@ -98,11 +96,19 @@ .join(', ')} {#if services.length} - - {#if $organization?.billingPlan !== BillingPlan.FREE && hasUsageFees} + {@const supportsUsage = Object.keys($currentPlan.usage).length > 0} + + {#if !supportsUsage && hasUsageFees} - You've reached the {services} limit for the {tier} plan. + You've reached the {services} limit for the {planName} plan. ($showUsageRatesModal = true)} >Excess usage fees will apply. @@ -111,8 +117,8 @@ {:else} - You've reached the {services} limit for the {tier} plan. Upgrade your organization for additional resources. @@ -135,7 +141,7 @@ on:click={() => (showDropdown = !showDropdown)}> - {:else if $organization?.billingPlan !== BillingPlan.SCALE} + {:else} {/if} - + {#if hasProjectLimitation}

    You are limited to {limit} - {title.toLocaleLowerCase()} per project on the {tier} plan. - {#if $organization?.billingPlan === BillingPlan.FREE}Upgrade @@ -160,7 +172,7 @@ {:else if hasUsageFees}

    You are limited to {limit} - {title.toLocaleLowerCase()} per organization on the {tier} plan. + {title.toLocaleLowerCase()} per organization on the {planName} plan. ($showUsageRatesModal = true)} >Excess usage fees will apply. @@ -168,9 +180,9 @@ {:else}

    You are limited to {limit} - {title.toLocaleLowerCase()} per organization on the {tier} plan. - {#if $organization?.billingPlan === BillingPlan.FREE} - Upgrade + {title.toLocaleLowerCase()} per organization on the {planName} plan. + {#if canUpgrade($organization.billingPlanId)} + Upgrade for additional {title.toLocaleLowerCase()}. {/if}

    diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index c24e9234aa..5117b5738b 100644 --- a/src/lib/layout/createProject.svelte +++ b/src/lib/layout/createProject.svelte @@ -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; 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 + ); + }); @@ -61,7 +62,7 @@ 0} Region cannot be changed after creation {/if} + {#if isAddonProject} Each added project comes with its own dedicated pool of resources. {/if} + {#if projectsLimited} + title={`You've reached your limit of ${currentPlan?.projects} projects`}> Extra projects are available on paid plans for an additional fee diff --git a/src/lib/layout/shell.svelte b/src/lib/layout/shell.svelte index fcfc39086d..1ce8a81379 100644 --- a/src/lib/layout/shell.svelte +++ b/src/lib/layout/shell.svelte @@ -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 }; }), diff --git a/src/lib/layout/unauthenticated.svelte b/src/lib/layout/unauthenticated.svelte index bccb4de76e..27b44f624e 100644 --- a/src/lib/layout/unauthenticated.svelte +++ b/src/lib/layout/unauthenticated.svelte @@ -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]; diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 048dbb3d12..2c7a646d4b 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -1,333 +1,4 @@ -import type { Tier } from '$lib/stores/billing'; -import type { Campaign } from '$lib/stores/campaigns'; -import type { Client, Models } from '@appwrite.io/console'; -import type { PaymentMethod } from '@stripe/stripe-js'; -import type { Organization, OrganizationError, OrganizationList } from '../stores/organization'; - -export type PaymentMethodData = { - $id: string; - $createdAt: string; - $updatedAt: string; - providerMethodId: string; - providerUserId: string; - userId: string; - expiryMonth: number; - expiryYear: number; - expired: boolean; - last4: string; - country: string; - brand: string; - clientSecret: string; - failed: boolean; - name: string; - mandateId?: string; - lastError?: string; - state?: string; -}; - -export type PaymentList = { - paymentMethods: PaymentMethodData[]; - total: number; -}; - -export type Invoice = { - $id: string; - $createdAt: Date; - $updatedAt: Date; - permissions: string[]; - teamId: string; - aggregationId: string; - plan: Tier; - amount: number; - tax: number; - taxAmount: number; - vat: number; - vatAmount: number; - grossAmount: number; - creditsUsed: number; - currency: string; - from: string; - to: string; - status: string; - dueAt: string; - clientSecret: string; - usage: { - name: string; - value: number /* service over the limit*/; - amount: number /* price of service over the limit*/; - rate: number; - desc: string; - }[]; - lastError?: string; -}; - -export type InvoiceList = { - invoices: Invoice[]; - total: number; -}; - -export type Estimation = { - amount: number; - grossAmount: number; - credits: number; - discount: number; - items: EstimationItem[]; - discounts: EstimationItem[]; - trialDays: number; - trialEndDate: string | undefined; - error: string | undefined; -}; - -export type EstimationItem = { - label: string; - value: number; -}; - -export type EstimationDeleteOrganization = { - amount: number; - grossAmount: number; - credits: number; - discount: number; - items: EstimationItem[]; - unpaidInvoices: Invoice[]; -}; - -export type Coupon = { - $id: string; - code: string; - credits: number; - expiration: string; - status: string; // 'active' | 'disabled' | 'expired' - validity: number; - campaign?: string; - onlyNewOrgs?: boolean; -}; - -export type Credit = { - /** - * Credit ID. - */ - $id: string; - /** - * Credit creation time in ISO 8601 format. - */ - $createdAt: string; - /** - * Credit update date in ISO 8601 format. - */ - $updatedAt: string; - /** - * coupon ID - */ - couponId: string; - /** - * ID of the User. - */ - userId: string; - /** - * ID of the Team. - */ - teamId: string; - /** - * Provided credit amount - */ - total: number; - /** - * Remaining credit amount - */ - credits: number; - /** - * Credit expiration time in ISO 8601 format. - */ - expiration: string; - /** - * Status of the credit. Can be one of `disabled`, `active` or `expired`. - */ - status: string; -}; - -export type CreditList = { - available: number; - credits: Credit[]; - total: number; -}; - -export type AggregationTeam = { - $id: string; - /** - * Aggregation creation time in ISO 8601 format. - */ - $createdAt: string; - /** - * Aggregation update date in ISO 8601 format. - */ - $updatedAt: string; - /** - * Beginning date of the invoice. - */ - from: string; - /** - * End date of the invoice. - */ - to: string; - /** - * Total amount of the invoice. - */ - amount: number; - additionalMembers: number; - - /** - * Price for additional members - */ - additionalMemberAmount: number; - /** - * Total storage usage. - */ - usageStorage: number; - /** - * Total active users for the billing period. - */ - usageUsers: number; - /** - * Total number of executions for the billing period. - */ - usageExecutions: number; - /** - * Total bandwidth usage for the billing period. - */ - usageBandwidth: number; - /** - * Total realtime usage for the billing period. - */ - usageRealtime: number; - /** - * Usage logs for the billing period. - */ - resources: InvoiceUsage[]; - /** - * Aggregation billing plan - */ - plan: string; - breakdown: AggregationBreakdown[]; -}; - -export type AggregationBreakdown = { - $id: string; - name: string; - amount: number; - region: string; - resources: InvoiceUsage[]; -}; - -export type InvoiceUsage = { - resourceId: string; - value: number; - amount: number; -}; - -export type AvailableCredit = { - available: number; -}; - -export type Aggregation = { - $id: string; - /** - * Aggregation creation time in ISO 8601 format. - */ - $createdAt: string; - /** - * Aggregation update date in ISO 8601 format. - */ - $updatedAt: string; - /** - * Beginning date of the invoice. - */ - from: string; - /** - * End date of the invoice. - */ - to: string; - /** - * Total amount of the invoice. - */ - amount: number; - additionalMembers: number; - - /** - * Price for additional members - */ - additionalMemberAmount: number; - /** - * Total storage usage. - */ - usageStorage: number; - /** - * Total active users for the billing period. - */ - usageUsers: number; - /** - * Total number of executions for the billing period. - */ - usageExecutions: number; - /** - * Total bandwidth usage for the billing period. - */ - usageBandwidth: number; - /** - * Total realtime usage for the billing period. - */ - usageRealtime: number; - /** - * Usage logs for the billing period. - */ - resources: OrganizationUsage; - /** - * Aggregation billing plan - */ - plan: string; -}; - -export type OrganizationUsage = { - bandwidth: Array; - executions: Array; - databasesReads: Array; - databasesWrites: Array; - imageTransformations: Array; - executionsTotal: number; - filesStorageTotal: number; - buildsStorageTotal: number; - databasesReadsTotal: number; - databasesWritesTotal: number; - imageTransformationsTotal: number; - screenshotsGeneratedTotal: number; - deploymentsStorageTotal: number; - executionsMBSecondsTotal: number; - buildsMBSecondsTotal: number; - backupsStorageTotal: number; - storageTotal: number; - users: Array; - usersTotal: number; - projects: Array<{ - projectId: string; - storage: number; - executions: number; - bandwidth: number; - databasesReads: number; - databasesWrites: number; - users: number; - authPhoneTotal: number; - authPhoneEstimate: number; - imageTransformations: number; - screenshotsGenerated: number; - }>; - authPhoneTotal: number; - authPhoneEstimate: number; -}; - -export type AggregationList = { - aggregations: Aggregation[]; - total: number; -}; +import type { Models } from '@appwrite.io/console'; export type AllowedRegions = | 'fra' @@ -341,1100 +12,4 @@ export type AllowedRegions = | 'syd' | 'default'; //TODO: remove after migration -export type Address = { - $id: string; - streetAddress: string; - addressLine2?: string; - country: string; - city: string; - state?: string; - postalCode: string; - userId: string; -}; - -export type AddressesList = { - billingAddresses: Address[]; - total: number; -}; - -export type AdditionalResource = { - name: string; - currency: string; - invoiceDesc: string; - price: number; - unit: string; - value: number; - multiplier?: number; -}; - -export type PlanAddon = { - supported: boolean; - currency: string; - invoiceDesc: string; - price: number; - limit: number; - value: number; - type: string; - planIncluded: number; -}; - -export type Plan = { - $id: string; - name: string; - desc: string; - price: number; - order: number; - bandwidth: number; - storage: number; - imageTransformations: number; - screenshotsGenerated: number; - webhooks: number; - users: number; - teams: number; - projects: number; - databases: number; - databasesAllowEncrypt: boolean; - databasesReads: number; - databasesWrites: number; - buckets: number; - fileSize: number; - functions: number; - executions: number; - GBHours: number; - realtime: number; - logs: number; - authPhone: number; - usage: { - bandwidth: AdditionalResource; - executions: AdditionalResource; - member: AdditionalResource; - realtime: AdditionalResource; - storage: AdditionalResource; - users: AdditionalResource; - databasesReads: AdditionalResource; - databasesWrites: AdditionalResource; - GBHours: AdditionalResource; - imageTransformations: AdditionalResource; - }; - addons: { - seats: PlanAddon; - projects: PlanAddon; - }; - trialDays: number; - budgetCapEnabled: boolean; - isAvailable: boolean; - selfService: boolean; - premiumSupport: boolean; - budgeting: boolean; - supportsMockNumbers: boolean; - backupsEnabled: boolean; - backupPolicies: number; - emailBranding: boolean; - supportsCredits: boolean; - supportsOrganizationRoles: boolean; - buildSize: number; // in MB - deploymentSize: number; // in MB - usagePerProject: boolean; -}; - -export type PlanList = { - plans: Plan[]; - total: number; -}; - -export type PlansMap = Map; - -export type Roles = { - scopes: string[]; - roles: string[]; -}; - -export class Billing { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - async listOrganization(queries: string[] = []): Promise { - const path = `/organizations`; - const params = { - queries - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async validateOrganization(organizationId: string, invites: string[]): Promise { - const path = `/organizations/${organizationId}/validate`; - const params = { - organizationId, - invites - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'PATCH', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async createOrganization( - organizationId: string, - name: string, - billingPlan: string, - paymentMethodId: string, - billingAddressId: string = undefined, - couponId: string = null, - invites: Array = [], - budget: number = undefined, - taxId: string = null - ): Promise { - const path = `/organizations`; - const params = { - organizationId, - name, - billingPlan, - paymentMethodId, - billingAddressId, - couponId, - invites, - budget, - taxId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'POST', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async estimationCreateOrganization( - billingPlan: string, - couponId: string = null, - invites: Array = [] - ): Promise { - const path = `/organizations/estimations/create-organization`; - const params = { - billingPlan, - couponId, - invites - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async deleteOrganization(organizationId: string): Promise { - const path = `/organizations/${organizationId}`; - const params = { - organizationId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'DELETE', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async estimationDeleteOrganization( - organizationId: string - ): Promise { - const path = `/organizations/${organizationId}/estimations/delete-organization`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('patch', uri, { - 'content-type': 'application/json' - }); - } - - async getOrganizationPlan(organizationId: string): Promise { - const path = `/organizations/${organizationId}/plan`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('get', uri, { - 'content-type': 'application/json' - }); - } - - async listPlans(queries: string[] = []): Promise { - const path = `/console/plans`; - const uri = new URL(this.client.config.endpoint + path); - const params = { - queries - }; - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getPlan(planId: string): Promise { - const path = `/console/plans/${planId}`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('get', uri, { - 'content-type': 'application/json' - }); - } - - async getRoles(organizationId: string): Promise { - const path = `/organizations/${organizationId}/roles`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('get', uri, { - 'content-type': 'application/json' - }); - } - - async updatePlan( - organizationId: string, - billingPlan: string, - paymentMethodId: string, - billingAddressId: string = undefined, - couponId: string = null, - invites: Array = [], - budget: number = undefined, - taxId: string = null - ): Promise { - const path = `/organizations/${organizationId}/plan`; - const params = { - organizationId, - billingPlan, - paymentMethodId, - billingAddressId, - couponId, - invites, - budget, - taxId - }; - - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async estimationUpdatePlan( - organizationId: string, - billingPlan: string, - couponId: string = null, - invites: Array = [] - ): Promise { - const path = `/organizations/${organizationId}/estimations/update-plan`; - const params = { - billingPlan, - couponId, - invites - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async cancelDowngrade(organizationId: string): Promise { - const path = `/organizations/${organizationId}/plan/cancel`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('patch', uri, { - 'content-type': 'application/json' - }); - } - - async updateBudget( - organizationId: string, - budget: number, - alerts: number[] - ): Promise { - const path = `/organizations/${organizationId}/budget`; - const params = { - organizationId, - budget, - alerts - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async updateSelectedProjects( - organizationId: string, - projects: string[] - ): Promise { - const path = `/organizations/${organizationId}/projects`; - const params = { - projects - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async setOrganizationPaymentMethod( - organizationId: string, - paymentMethodId: string - ): Promise { - const path = `/organizations/${organizationId}/payment-method`; - const params = { - organizationId, - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async setOrganizationPaymentMethodBackup( - organizationId: string, - paymentMethodId: string - ): Promise { - const path = `/organizations/${organizationId}/payment-method/backup`; - const params = { - organizationId, - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async removeOrganizationPaymentMethod(organizationId: string): Promise { - const path = `/organizations/${organizationId}/payment-method`; - const params = { - organizationId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'DELETE', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async removeOrganizationPaymentMethodBackup(organizationId: string): Promise { - const path = `/organizations/${organizationId}/payment-method/backup`; - const params = { - organizationId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'DELETE', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async listInvoices(organizationId: string, queries: string[] = []): Promise { - const path = `/organizations/${organizationId}/invoices`; - const params = { - organizationId, - queries - }; - - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getInvoice(organizationId: string, invoiceId: string): Promise { - const path = `/organizations/${organizationId}/invoices/${invoiceId}`; - const params = { - organizationId, - invoiceId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getInvoiceView( - organizationId: string, - invoiceId: string - ): Promise> { - const path = `/organizations/${organizationId}/invoices/${invoiceId}/view`; - const params = { - organizationId, - invoiceId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/pdf' - }, - params - ); - } - - async downloadInvoice( - organizationId: string, - invoiceId: string - ): Promise> { - const path = `/organizations/${organizationId}/invoices/${invoiceId}/download`; - const params = { - organizationId, - invoiceId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async updateInvoiceStatus(organizationId: string, invoiceId: string): Promise { - const path = `/organizations/${organizationId}/invoices/${invoiceId}/status`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('PATCH', uri, { - 'content-type': 'application/json' - }); - } - - async retryPayment( - organizationId: string, - invoiceId: string, - paymentMethodId: string - ): Promise { - const path = `/organizations/${organizationId}/invoices/${invoiceId}/payments`; - const params = { - organizationId, - invoiceId, - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'post', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async listUsage( - organizationId: string, - startDate: string = undefined, - endDate: string = undefined - ): Promise { - const path = `/organizations/${organizationId}/usage`; - const params = { - organizationId - }; - - if (startDate !== undefined) { - params['startDate'] = startDate; - } - if (endDate !== undefined) { - params['endDate'] = endDate; - } - - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async listAggregation( - organizationId: string, - queries: string[] = [] - ): Promise { - const path = `/organizations/${organizationId}/aggregations`; - const params = { - organizationId, - queries - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getAggregation( - organizationId: string, - aggregationId: string, - limit?: number, - offset?: number - ): Promise { - const path = `/organizations/${organizationId}/aggregations/${aggregationId}`; - const params: { - organizationId: string; - aggregationId: string; - limit?: number; - offset?: number; - } = { - organizationId, - aggregationId - }; - if (typeof limit === 'number') params.limit = limit; - if (typeof offset === 'number') params.offset = offset; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async addCredit(organizationId: string, couponId: string): Promise { - const path = `/organizations/${organizationId}/credits`; - const params = { - couponId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'POST', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async listCredits(organizationId: string, queries = []): Promise { - const path = `/organizations/${organizationId}/credits`; - const params = { - queries - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getAvailableCredit(organizationId: string): Promise { - const path = `/organizations/${organizationId}/credits/available`; - const params = {}; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getCredit(organizationId: string, creditId: string): Promise { - const path = `/organizations/${organizationId}/credits/${creditId}`; - const params = { - creditId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getCampaign(campaignId: string): Promise { - const path = `/console/campaigns/${campaignId}`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('GET', uri, { - 'content-type': 'application/json' - }); - } - - async setMembership( - programId: string - ): Promise<{ $createdAt: string } | { error: { code: number; message: string } }> { - const path = `/console/programs/${programId}/memberships`; - const uri = new URL(this.client.config.endpoint + path); - try { - return await this.client.call('POST', uri, { - 'content-type': 'application/json' - }); - } catch (e) { - return { error: { code: e.code, message: e.message } }; - } - } - - async getCouponAccount(couponId: string): Promise { - const path = `/account/coupons/${couponId}`; - const params = { - couponId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getCoupon(couponId: string): Promise { - const path = `/console/coupons/${couponId}`; - const params = { - couponId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async setBillingAddress( - organizationId: string, - billingAddressId: string - ): Promise { - const path = `/organizations/${organizationId}/billing-address`; - const params = { - organizationId, - billingAddressId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'PATCH', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async removeBillingAddress(organizationId: string): Promise { - const path = `/organizations/${organizationId}/billing-address`; - const params = { - organizationId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'DELETE', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async updateTaxId(organizationId: string, taxId: string): Promise { - const path = `/organizations/${organizationId}/taxId`; - const params = { - organizationId, - taxId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'PATCH', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getOrganizationPaymentMethod( - organizationId: string, - paymentMethodId: string - ): Promise { - const path = `/organizations/${organizationId}/payment-methods/${paymentMethodId}`; - const params = { - organizationId, - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getOrganizationBillingAddress( - organizationId: string, - billingAddressId: string - ): Promise
    { - const path = `/organizations/${organizationId}/billing-addresses/${billingAddressId}`; - const params = { - organizationId, - billingAddressId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - //ACCOUNT - - async listPaymentMethods(queries: [] = []): Promise { - const path = `/account/payment-methods`; - const params = { - queries - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getPaymentMethod(paymentMethodId: string): Promise { - const path = `/account/payment-methods/${paymentMethodId}`; - const params = { - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async createPaymentMethod(): Promise { - const path = `/account/payment-methods`; - const params = {}; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'POST', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async setPaymentMethod( - paymentMethodId: string, - providerMethodId: string | PaymentMethod, - name: string, - state: string | undefined = undefined - ): Promise { - const path = `/account/payment-methods/${paymentMethodId}/provider`; - const params = { - paymentMethodId, - providerMethodId, - name - }; - - if (state !== undefined) { - params['state'] = state; - } - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async updatePaymentMethod( - paymentMethodId: string, - expiryMonth: string, - expiryYear: string - ): Promise { - const path = `/account/payment-methods/${paymentMethodId}`; - const params = { - paymentMethodId, - expiryMonth, - expiryYear - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async deletePaymentMethod(paymentMethodId: string): Promise { - const path = `/account/payment-methods/${paymentMethodId}`; - const params = { - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'delete', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async setDefaultPaymentMethod(paymentMethodId: string): Promise { - const path = `/account/payment-methods/${paymentMethodId}/default`; - const params = { - paymentMethodId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'patch', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async listAddresses(queries: string[] = []): Promise { - const path = `/account/billing-addresses`; - const params = { - queries - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getAddress(billingAddressId: string): Promise
    { - const path = `/account/billing-addresses/${billingAddressId}`; - const params = { - billingAddressId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async createAddress( - country: string, - streetAddress: string, - city: string, - state: string, - postalCode: string, - addressLine2?: string - ): Promise
    { - const path = `/account/billing-addresses`; - const params = { - country, - streetAddress, - city, - state, - postalCode, - addressLine2 - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'POST', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async updateAddress( - billingAddressId: string, - country: string, - streetAddress: string, - city: string, - state: string, - postalCode: string, - addressLine2?: string - ): Promise
    { - const path = `/account/billing-addresses/${billingAddressId}`; - const params = { - billingAddressId, - country, - streetAddress, - city, - state, - postalCode, - addressLine2 - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'PUT', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async deleteAddress(billingAddressId: string): Promise { - const path = `/account/billing-addresses/${billingAddressId}`; - const params = { - billingAddressId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'delete', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async listRegions(organizationId: string): Promise { - const path = `/organizations/${organizationId}/regions`; - const params = { - organizationId - }; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - - async getPlansInfo(): Promise { - const path = `/console/plans`; - const params = {}; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'GET', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } -} +export type BillingPlansMap = Map; diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index df2d291788..baf663114b 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -1,48 +1,39 @@ -import { browser } from '$app/environment'; +import { resolve } from '$app/paths'; +import { page } from '$app/stores'; import { goto } from '$app/navigation'; -import { base } from '$app/paths'; +import { browser } from '$app/environment'; import { Click, trackEvent } from '$lib/actions/analytics'; -import { page } from '$app/stores'; 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(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(false); +export const daysLeftInTrial = writable(0); +export const plansInfo = writable(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; } @@ -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(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(null); export const showUsageRatesModal = writable(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 = `${org.name} has reached 75% 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 ${org.name} 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 = `${organization.name} has reached ${threshold}% of its ${exceededResources[0].name} limit. Upgrade to ensure there are no service disruptions.`; + + if (exceededResources.length > 1) { + message = `Usage for ${organization.name} has reached ${threshold}% 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 */ + } + } -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; + if (orgId) { + return resolve('/(console)/organization-[organization]/change-plan', { + organization: orgId + }); + } else { + /* fallback to not crash anything */ + return resolve('/'); + } } -export function isOrganization(org: Organization | OrganizationError): org is Organization { - return (org as Organization).$id !== undefined; +export const hideBillingHeaderRoutes = [resolve('/account'), resolve('/create-organization')]; + +export function isPaymentAuthenticationRequired( + org: Models.Organization | Models.PaymentAuthentication +): org is Models.PaymentAuthentication { + return 'clientSecret' in org; } diff --git a/src/lib/stores/bottom-alerts.ts b/src/lib/stores/bottom-alerts.ts index 0e80a6dfa8..b0a044f1a6 100644 --- a/src/lib/stores/bottom-alerts.ts +++ b/src/lib/stores/bottom-alerts.ts @@ -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; }; diff --git a/src/lib/stores/campaigns.ts b/src/lib/stores/campaigns.ts deleted file mode 100644 index cf082fb94a..0000000000 --- a/src/lib/stores/campaigns.ts +++ /dev/null @@ -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; -}; diff --git a/src/lib/stores/organization.ts b/src/lib/stores/organization.ts index b3b874cab4..8503d8f637 100644 --- a/src/lib/stores/organization.ts +++ b/src/lib/stores/organization.ts @@ -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> & { - 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(false); export const newMemberModal = writable(false); @@ -62,7 +11,26 @@ export const organizationList = derived( ($page) => $page.data.organizations as Models.TeamList> ); -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({ total: 0, regions: [] }); + +export async function getTeamOrOrganizationList( + queries: string[] = [] +): Promise { + 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; +} diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index b932c7e12c..c2ff97c857 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -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), diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index 9f4bc727bc..01a824a539 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -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(); -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 -) { +export async function confirmPayment(config: { + clientSecret: string; + paymentMethodId: string; + orgId?: string; + route?: string; +}) { + const { clientSecret, paymentMethodId, orgId, route } = config; + try { - const url = - window.location.origin + (route ? route : `${base}/organization-${orgId}/billing`); + const resolvedUrl = resolve('/(console)/organization-[organization]/billing', { + organization: orgId + }); + + const url = window.location.origin + (route ? route : resolvedUrl); - const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentMethodId); + 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; } diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index ce28dd7c2f..1b0230829a 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -1,12 +1,12 @@ diff --git a/src/routes/(console)/account/payments/+page.ts b/src/routes/(console)/account/payments/+page.ts index b275a6e9b5..fb0cb2d96d 100644 --- a/src/routes/(console)/account/payments/+page.ts +++ b/src/routes/(console)/account/payments/+page.ts @@ -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() ]); diff --git a/src/routes/(console)/account/payments/addressModal.svelte b/src/routes/(console)/account/payments/addressModal.svelte index 5b058dbfb0..57ea22c101 100644 --- a/src/routes/(console)/account/payments/addressModal.svelte +++ b/src/routes/(console)/account/payments/addressModal.svelte @@ -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); } diff --git a/src/routes/(console)/account/payments/billingAddress.svelte b/src/routes/(console)/account/payments/billingAddress.svelte index 4e55ddb1a7..5e9b888376 100644 --- a/src/routes/(console)/account/payments/billingAddress.svelte +++ b/src/routes/(console)/account/payments/billingAddress.svelte @@ -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 = []; let showDelete = false; - $: orgList = $organizationList.teams as unknown as Organization[]; + $: orgList = $organizationList.teams as unknown as Array; diff --git a/src/routes/(console)/account/payments/deleteAddressModal.svelte b/src/routes/(console)/account/payments/deleteAddressModal.svelte index 059beab3cb..9f267daabb 100644 --- a/src/routes/(console)/account/payments/deleteAddressModal.svelte +++ b/src/routes/(console)/account/payments/deleteAddressModal.svelte @@ -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 = []; + 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({ diff --git a/src/routes/(console)/account/payments/deletePaymentModal.svelte b/src/routes/(console)/account/payments/deletePaymentModal.svelte index 50406a32f5..613db91617 100644 --- a/src/routes/(console)/account/payments/deletePaymentModal.svelte +++ b/src/routes/(console)/account/payments/deletePaymentModal.svelte @@ -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 = []; 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({ diff --git a/src/routes/(console)/account/payments/editAddressModal.svelte b/src/routes/(console)/account/payments/editAddressModal.svelte index 59b709bdd5..891b65103f 100644 --- a/src/routes/(console)/account/payments/editAddressModal.svelte +++ b/src/routes/(console)/account/payments/editAddressModal.svelte @@ -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) { diff --git a/src/routes/(console)/account/payments/editPaymentModal.svelte b/src/routes/(console)/account/payments/editPaymentModal.svelte index 24a8fa08f3..eb6dedcc9f 100644 --- a/src/routes/(console)/account/payments/editPaymentModal.svelte +++ b/src/routes/(console)/account/payments/editPaymentModal.svelte @@ -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); diff --git a/src/routes/(console)/account/payments/paymentMethods.svelte b/src/routes/(console)/account/payments/paymentMethods.svelte index 9caea2e1f5..ca823e637d 100644 --- a/src/routes/(console)/account/payments/paymentMethods.svelte +++ b/src/routes/(console)/account/payments/paymentMethods.svelte @@ -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 = []; 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; - $: 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 diff --git a/src/routes/(console)/apply-credit/+page.svelte b/src/routes/(console)/apply-credit/+page.svelte index db9dbc0387..e2ad497d54 100644 --- a/src/routes/(console)/apply-credit/+page.svelte +++ b/src/routes/(console)/apply-credit/+page.svelte @@ -1,26 +1,26 @@ @@ -283,7 +314,9 @@ placeholder="Select organization" id="organization" /> {/if} - {#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)} + + + {#if selectedOrgId && !selectedOrg?.billingPlanDetails.addons.seats.supported} {#if selectedOrgId === newOrgId} {/if} + - {#if (selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)) || (!data?.couponData?.code && selectedOrgId)} + + {#if showPaymentSection}
    - {#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)} + {#if needsPaymentMethod} {/if}
    - {#if !data?.couponData?.code && selectedOrgId} + {#if needsCouponInput} - {#if selectedOrg?.$id && selectedOrg?.billingPlan === billingPlan} + {#if selectedOrg?.$id && selectedOrg?.billingPlanId === billingPlan.$id}
    { // 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('/')); } }; diff --git a/src/routes/(console)/create-organization/+page.svelte b/src/routes/(console)/create-organization/+page.svelte index 13d296facd..bf5a4a1098 100644 --- a/src/routes/(console)/create-organization/+page.svelte +++ b/src/routes/(console)/create-organization/+page.svelte @@ -1,20 +1,22 @@ @@ -117,41 +131,43 @@ {/if} {#if organization?.billingPlanDowngrade} - 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 + )}. {/if} {#if $useNewPricingModal} + availableCredit={data.availableCredit} + currentPlan={data.currentPlan} + nextPlan={data.nextPlan} + currentAggregation={data.billingAggregation} + limit={data.limit} + offset={data.offset} /> {:else} + availableCredit={data.availableCredit} + currentPlan={data.currentPlan} + currentAggregation={data.billingAggregation} + currentInvoice={data.billingInvoice} /> {/if} + locale={data.locale} + countryList={data.countryList} + organization={data.organization} + billingAddress={data.billingAddress} /> - + diff --git a/src/routes/(console)/organization-[organization]/billing/+page.ts b/src/routes/(console)/organization-[organization]/billing/+page.ts index 2f2a4418cf..4c10658caa 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.ts +++ b/src/routes/(console)/organization-[organization]/billing/+page.ts @@ -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
    = billingAddressId - ? sdk.forConsole.billing - .getOrganizationBillingAddress(organization.$id, billingAddressId) + const billingAddressId = (organization as Models.Organization)?.billingAddressId; + const billingAddressPromise: Promise = 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) { diff --git a/src/routes/(console)/organization-[organization]/billing/addCreditModal.svelte b/src/routes/(console)/organization-[organization]/billing/addCreditModal.svelte index 263e9a737a..be8a77303f 100644 --- a/src/routes/(console)/organization-[organization]/billing/addCreditModal.svelte +++ b/src/routes/(console)/organization-[organization]/billing/addCreditModal.svelte @@ -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); diff --git a/src/routes/(console)/organization-[organization]/billing/addCreditWizard.svelte b/src/routes/(console)/organization-[organization]/billing/addCreditWizard.svelte index 3ae09404f6..cff0627255 100644 --- a/src/routes/(console)/organization-[organization]/billing/addCreditWizard.svelte +++ b/src/routes/(console)/organization-[organization]/billing/addCreditWizard.svelte @@ -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}` diff --git a/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte b/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte index 52bc768599..93131601b2 100644 --- a/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte +++ b/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte @@ -1,18 +1,16 @@ - + {!areCreditsSupported ? 'Credits' : 'Available credit'} @@ -171,10 +171,10 @@ {/if} - {#if $organization?.billingPlan === BillingPlan.FREE} + {#if !areCreditsSupported} - {#if methods.total} + {#if paymentMethods.total} {#each filteredPaymentMethods as paymentMethod} addPaymentMethod(paymentMethod?.$id)}> @@ -335,18 +347,24 @@ selectedPaymentMethod={isSelectedBackup ? backupMethod : primaryMethod} /> {/if} {#if isCloud && hasStripePublicKey} - + {/if} {#if showDelete && isCloud && hasStripePublicKey} {@const hasOtherMethod = isSelectedBackup ? !!organization?.paymentMethodId : !!organization?.backupPaymentMethodId} + + disabled={$currentPlan.requiresPaymentMethod && !hasOtherMethod} /> {/if} + {#if showUpdateState && paymentMethodNeedingState && isCloud && hasStripePublicKey} {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index baec58e475..afe13b5172 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -3,11 +3,10 @@ import { EstimatedCard, Pagination as PaginationComponent } from '$lib/components'; import { Button } from '$lib/elements/forms'; import { toLocaleDate } from '$lib/helpers/date'; - import { upgradeURL } from '$lib/stores/billing'; + import { getChangePlanUrl } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; - import type { AggregationTeam, InvoiceUsage, Plan } from '$lib/sdk/billing'; import { formatCurrency } from '$lib/helpers/numbers'; - import { BillingPlan, DEFAULT_BILLING_PROJECTS_LIMIT } from '$lib/constants'; + import { DEFAULT_BILLING_PROJECTS_LIMIT } from '$lib/constants'; import { Click, trackEvent } from '$lib/actions/analytics'; import { Typography, @@ -25,6 +24,7 @@ import { IconTag } from '@appwrite.io/pink-icons-svelte'; import { page } from '$app/state'; import type { RowFactoryOptions } from '$routes/(console)/organization-[organization]/billing/store'; + import type { Models } from '@appwrite.io/console'; let { currentPlan, @@ -34,10 +34,10 @@ limit = undefined, offset = undefined }: { - currentPlan: Plan; - nextPlan?: Plan | null; + currentPlan: Models.BillingPlan; + nextPlan?: Models.BillingPlan | null; availableCredit?: number | undefined; - currentAggregation?: AggregationTeam | undefined; + currentAggregation?: Models.AggregationTeam | undefined; limit?: number | undefined; offset?: number | undefined; } = $props(); @@ -146,8 +146,8 @@ ]; } - function getResource(resources: InvoiceUsage[] | undefined, resourceId: string) { - return resources?.find((r) => r.resourceId === resourceId); + function getResource(resources: Array | undefined, resourceId: string) { + return resources?.find((resource) => resource.resourceId === resourceId); } function createRow({ @@ -221,7 +221,7 @@ function createResourceRow( id: string, label: string, - resource: InvoiceUsage | undefined, + resource: Models.UsageResources | undefined, planLimit: number | null | undefined, formatValue = formatNum ) { @@ -229,8 +229,8 @@ } function getBillingData( - currentPlan: Plan, - currentAggregation: AggregationTeam | undefined, + currentPlan: Models.BillingPlan, + currentAggregation: Models.AggregationTeam | undefined, isSmallViewport: boolean ) { // base plan row @@ -588,7 +588,7 @@
    - {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} + {#if !currentPlan.requiresPaymentMethod} trackEvent(Click.OrganizationClickUpgrade, { from: 'button', @@ -626,7 +626,7 @@
    {/if} - {#if isDowngrade && selectedPlan === BillingPlan.FREE && !data.hasFreeOrgs} + {#if isDowngrade && selectedPlan.group === BillingPlanGroup.Starter && !data.hasFreeOrgs}
    - {#if selectedPlan !== BillingPlan.FREE && data.organization.billingPlan !== selectedPlan && data.organization.billingPlan !== BillingPlan.CUSTOM} + {@const isStarter = selectedPlan.group === BillingPlanGroup.Starter} + {@const isSelfService = data.organization.billingPlanDetails.selfService} + {@const isSameGroup = data.organization.billingPlanDetails.group === selectedPlan.group} + {#if !isStarter && !isSameGroup && !isSelfService} - {:else if data.organization.billingPlan !== BillingPlan.CUSTOM} + {:else if !data.organization.billingPlanDetails.selfService} {/if} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index c6a137f96a..e8fa3cd3ad 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -1,31 +1,44 @@ import type { PageLoad } from './$types'; -import type { Organization } from '$lib/stores/organization'; -import { BillingPlan, Dependencies } from '$lib/constants'; +import { Dependencies } from '$lib/constants'; import { sdk } from '$lib/stores/sdk'; +import { BillingPlanGroup, type Models, Platform } from '@appwrite.io/console'; +import { getBasePlanFromGroup } from '$lib/stores/billing'; export const load: PageLoad = async ({ depends, parent }) => { const { members, currentPlan, organizations } = await parent(); + depends(Dependencies.UPGRADE_PLAN); - let plans; + let plans: Models.BillingPlanList; + try { - plans = await sdk.forConsole.billing.listPlans(); + plans = await sdk.forConsole.console.getPlans({ + platform: Platform.Appwrite + }); } catch (error) { console.error('Failed to load billing plans:', error); - plans = { plans: {} }; + plans = { + total: 0, + plans: [] + }; } - let plan: BillingPlan; + let plan: Models.BillingPlan; + + const pro = getBasePlanFromGroup(BillingPlanGroup.Pro); + const scale = getBasePlanFromGroup(BillingPlanGroup.Scale); - if (currentPlan?.$id === BillingPlan.SCALE) { - plan = BillingPlan.SCALE; + if (currentPlan?.group === BillingPlanGroup.Scale) { + plan = scale; } else { - plan = BillingPlan.PRO; + plan = pro; } const selfService = currentPlan?.selfService ?? true; const hasFreeOrgs = organizations.teams?.some( - (org) => (org as Organization)?.billingPlan === BillingPlan.FREE + (org) => + (org as Models.Organization)?.billingPlanId === + getBasePlanFromGroup(BillingPlanGroup.Starter).$id ); return { diff --git a/src/routes/(console)/organization-[organization]/createProjectCloud.svelte b/src/routes/(console)/organization-[organization]/createProjectCloud.svelte index 2b740800e9..f558211997 100644 --- a/src/routes/(console)/organization-[organization]/createProjectCloud.svelte +++ b/src/routes/(console)/organization-[organization]/createProjectCloud.svelte @@ -1,29 +1,37 @@ + title="Create project" + bind:show={showCreateProjectCloud}> + diff --git a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.svelte b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.svelte index fc70514a72..fb630bac94 100644 --- a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.svelte +++ b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.svelte @@ -1,15 +1,15 @@ - + + diff --git a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.ts b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.ts index fda7fb6a2a..2ed1fc7d71 100644 --- a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.ts +++ b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.ts @@ -1,18 +1,26 @@ -import { Dependencies } from '$lib/constants'; -import { sdk } from '$lib/stores/sdk'; import { isCloud } from '$lib/system'; -import { Query, Platform } from '@appwrite.io/console'; +import { redirect } from '@sveltejs/kit'; +import { resolve } from '$app/paths'; +import { Dependencies } from '$lib/constants'; +import type { Models } from '@appwrite.io/console'; +import { getTeamOrOrganizationList } from '$lib/stores/organization'; -export const load = async ({ parent, depends }) => { - depends(Dependencies.DOMAINS); +export const load = async ({ params, parent, depends }) => { + if (!isCloud) { + redirect( + 303, + resolve('/(console)/organization-[organization]', { + organization: params.organization + }) + ); + } - const organizations = !isCloud - ? await sdk.forConsole.teams.list() - : await sdk.forConsole.billing.listOrganization([ - Query.equal('platform', Platform.Appwrite) - ]); + depends(Dependencies.DOMAINS); const { domain } = await parent(); + + const organizations = (await getTeamOrOrganizationList()) as Models.OrganizationList; + return { domain, organizations diff --git a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/changeOrganization.svelte b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/changeOrganization.svelte index 542ea62ad1..0ecaa38cb8 100644 --- a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/changeOrganization.svelte +++ b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/changeOrganization.svelte @@ -6,12 +6,12 @@ import { Dependencies } from '$lib/constants'; import { Button, InputSelect } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; - import type { OrganizationList } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; import type { Models } from '@appwrite.io/console'; export let domain: Models.Domain; - export let organizations: OrganizationList; + export let organizations: Models.OrganizationList; + let selectedOrg: string = null; async function moveDomain() { diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte index 191131ae49..2062f153ae 100644 --- a/src/routes/(console)/organization-[organization]/header.svelte +++ b/src/routes/(console)/organization-[organization]/header.svelte @@ -1,26 +1,14 @@ {#if estimation} diff --git a/src/routes/(console)/organization-[organization]/settings/deleteOrganizationModal.svelte b/src/routes/(console)/organization-[organization]/settings/deleteOrganizationModal.svelte index 135d9b68c4..51a1d483aa 100644 --- a/src/routes/(console)/organization-[organization]/settings/deleteOrganizationModal.svelte +++ b/src/routes/(console)/organization-[organization]/settings/deleteOrganizationModal.svelte @@ -12,23 +12,25 @@ import { toLocaleDate } from '$lib/helpers/date'; import { isCloud } from '$lib/system'; import { formatCurrency } from '$lib/helpers/numbers'; - import { tierToPlan } from '$lib/stores/billing'; + import { billingIdToPlan } from '$lib/stores/billing'; import { Table, Tabs, Alert } from '@appwrite.io/pink-svelte'; import DeleteOrganizationEstimation from './deleteOrganizationEstimation.svelte'; - import type { EstimationDeleteOrganization, InvoiceList } from '$lib/sdk/billing'; + import type { Models } from '@appwrite.io/console'; export let showDelete = false; - export let invoices: InvoiceList; + export let invoices: Models.InvoiceList; let error: string = null; let selectedTab = 'projects'; let organizationName: string = null; - let estimation: EstimationDeleteOrganization; + let estimation: Models.EstimationDeleteOrganization; async function deleteOrg() { try { if (isCloud) { - await sdk.forConsole.billing.deleteOrganization($organization.$id); + await sdk.forConsole.organizations.delete({ + organizationId: $organization.$id + }); } else { await sdk.forConsole.teams.delete({ teamId: $organization.$id @@ -82,9 +84,9 @@ if (isCloud) { try { error = ''; - estimation = await sdk.forConsole.billing.estimationDeleteOrganization( - $organization.$id - ); + estimation = await sdk.forConsole.organizations.estimationDeleteOrganization({ + organizationId: $organization.$id + }); } catch (e) { error = e.message; } @@ -97,7 +99,7 @@ + ${billingIdToPlan(upcomingInvoice.plan).name} plan`}> By proceeding, your invoice will be processed within the hour. Upon successful payment, your organization will be deleted. @@ -112,7 +114,8 @@ This action is irreversible. {/if}

    - {#if estimation && (estimation.unpaidInvoices.length > 0 || estimation.grossAmount > 0)} + + {#if estimation && (estimation.unpaidInvoices.length > 0 || estimation.unpaidInvoices.some((invoice) => invoice.grossAmount > 0))} {:else} {#if $projects.total > 0} diff --git a/src/routes/(console)/organization-[organization]/settings/invoicesTable.svelte b/src/routes/(console)/organization-[organization]/settings/invoicesTable.svelte index 84b7ca6093..514e8a575a 100644 --- a/src/routes/(console)/organization-[organization]/settings/invoicesTable.svelte +++ b/src/routes/(console)/organization-[organization]/settings/invoicesTable.svelte @@ -1,5 +1,4 @@
    Usage - {#if $organization?.billingPlan === BillingPlan.FREE} + {#if isStarterPlan(currentBillingPlan)} {/if}
    - {#if $organization.billingPlan === BillingPlan.SCALE} + {#if planHasGroup(currentBillingPlan, BillingPlanGroup.Scale)}

    On the Scale plan, you'll be charged only for any usage that exceeds the thresholds per resource listed below. @@ -89,7 +91,7 @@ {/if}

    - {:else if $organization.billingPlan === BillingPlan.PRO} + {:else if planHasGroup(currentBillingPlan, BillingPlanGroup.Pro)}

    On the Pro plan, you'll be charged only for any usage that exceeds the thresholds per resource listed below. @@ -104,11 +106,13 @@ {/if}

    - {:else if $organization.billingPlan === BillingPlan.FREE} + {:else if isStarterPlan(currentBillingPlan)}

    If you exceed the limits of the Free plan, services for your organization's projects may be disrupted. - Upgrade for greater capacity. + Upgrade for greater capacity.

    {/if} diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts index 88d3194d07..6c6f9425cb 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.ts @@ -1,11 +1,10 @@ import { sdk } from '$lib/stores/sdk'; import type { PageLoad } from './$types'; -import type { UsageProjectInfo } from '../../store'; import { type Models, Query } from '@appwrite.io/console'; -import type { Invoice, OrganizationUsage } from '$lib/sdk/billing'; +import type { UsageProjectInfo } from '../../store'; export const load: PageLoad = async ({ params, parent }) => { - const { invoice } = params; + const { invoice: invoiceId } = params; const { organization: org, currentPlan: plan } = await parent(); /** @@ -43,16 +42,24 @@ export const load: PageLoad = async ({ params, parent }) => { let startDate: string = org.billingCurrentInvoiceDate; let endDate: string = org.billingNextInvoiceDate; - let currentInvoice: Invoice = undefined; + let currentInvoice: Models.Invoice = undefined; - if (invoice) { - currentInvoice = await sdk.forConsole.billing.getInvoice(org.$id, invoice); + if (invoiceId) { + currentInvoice = await sdk.forConsole.organizations.getInvoice({ + organizationId: org.$id, + invoiceId + }); startDate = currentInvoice.from; endDate = currentInvoice.to; } const [usage, organizationMembers] = await Promise.all([ - sdk.forConsole.billing.listUsage(org.$id, startDate, endDate), + sdk.forConsole.organizations.getUsage({ + organizationId: org.$id, + startDate, + endDate + }), + // this section is cloud only, // so it is fine to use this check and fetch memberships conditionally! !plan?.addons?.seats?.supported @@ -70,7 +77,7 @@ export const load: PageLoad = async ({ params, parent }) => { }; // all this to get the project's name and region! -function getUsageProjects(usage: OrganizationUsage) { +function getUsageProjects(usage: Models.UsageOrganization) { return (async () => { const limit = 100; const requests: Array> = []; diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte index 15d1180cfe..1531e85099 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte @@ -1,12 +1,12 @@ Members The number of members in your organization. - {#if $organization.billingPlan !== BillingPlan.FREE} + {#if !organizationMembersSupported}
    @@ -37,11 +35,7 @@ - You can add unlimited organization members on the {tierToPlan( - $organization.billingPlan - ).name} plan {$organization.billingPlan === BillingPlan.PRO - ? `for ${formatCurrency(plan.addons.seats.price)} each per billing period.` - : '.'} + You can add unlimited organization members on paid plans. @@ -53,7 +47,7 @@
    - {#snippet children(paginatedItems: typeof members.memberships)} + {#snippet children(paginatedItems)} Members @@ -86,7 +80,10 @@ {/snippet} {:else} - + {/if}
    diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index e45017ad88..3701c401ac 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -8,8 +8,7 @@ import { get } from 'svelte/store'; import { headerAlert } from '$lib/stores/headerAlert'; import PaymentFailed from '$lib/components/billing/alerts/paymentFailed.svelte'; import { loadAvailableRegions } from '$routes/(console)/regions'; -import type { Organization, OrganizationList } from '$lib/stores/organization'; -import { Platform } from '@appwrite.io/console'; +import { type Models, Platform } from '@appwrite.io/console'; import { redirect } from '@sveltejs/kit'; import { resolve } from '$app/paths'; @@ -21,19 +20,24 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { project.region ??= 'default'; // fast path without a network call! - let organization = (organizations as OrganizationList)?.teams?.find( + let organization = (organizations as Models.OrganizationList)?.teams?.find( (org) => org.$id === project.teamId ); // organization can be null if not in the filtered list! - const includedInBasePlans = plansInfo.has(organization?.billingPlan); + const includedInBasePlans = plansInfo.has(organization?.billingPlanId); const [org, regionalConsoleVariables, rolesResult] = await Promise.all([ !organization - ? (sdk.forConsole.teams.get({ teamId: project.teamId }) as Promise) + ? // TODO: @itznotabug - teams.get with Models.Organization? + (sdk.forConsole.teams.get({ teamId: project.teamId }) as Promise) : organization, sdk.forConsoleIn(project.region).console.variables(), - isCloud ? sdk.forConsole.billing.getRoles(project.teamId) : null, + isCloud + ? sdk.forConsole.organizations.getScopes({ + organizationId: project.teamId + }) + : null, loadAvailableRegions(project.teamId) ]); @@ -54,9 +58,11 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { // fetch if not available in `plansInfo`. // out of promise.all because we filter orgs based on platform now! const organizationPlan = includedInBasePlans - ? plansInfo.get(organization?.billingPlan) + ? plansInfo.get(organization?.billingPlanId) : isCloud - ? await sdk.forConsole.billing.getOrganizationPlan(organization?.$id) + ? await sdk.forConsole.organizations.getPlan({ + organizationId: organization?.$id + }) : null; const roles = rolesResult?.roles ?? defaultRoles; @@ -80,7 +86,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { if (!includedInBasePlans) { // save the custom plan to `plansInfo` cache. - plansInfo.set(organization.billingPlan, organizationPlan); + plansInfo.set(organization.billingPlanId, organizationPlan); } return { diff --git a/src/routes/(console)/project-[region]-[project]/auth/+layout.svelte b/src/routes/(console)/project-[region]-[project]/auth/+layout.svelte index 674167aa30..92f4242424 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/+layout.svelte @@ -1,111 +1,122 @@ Auth - Appwrite - +{@render children?.()} diff --git a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte index e8b9c7a298..e32114a8b6 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/+page.svelte @@ -34,7 +34,6 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { Dependencies } from '$lib/constants'; import { invalidate } from '$app/navigation'; - import type { PageProps } from './$types'; let { data }: PageProps = $props(); diff --git a/src/routes/(console)/project-[region]-[project]/auth/breadcrumbs.svelte b/src/routes/(console)/project-[region]-[project]/auth/breadcrumbs.svelte index db806d0381..f0806be09a 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/breadcrumbs.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/breadcrumbs.svelte @@ -1,8 +1,8 @@ - - - + + + - - + + diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte index bcd7116b8a..69c4deb979 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte @@ -7,16 +7,22 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { Selector } from '@appwrite.io/pink-svelte'; - import { project } from '../../store'; + import type { Models } from '@appwrite.io/console'; - let authMembershipsUserName = $project?.authMembershipsUserName ?? true; - let authMembershipsUserEmail = $project?.authMembershipsUserEmail ?? true; - let authMembershipsMfa = $project?.authMembershipsMfa ?? true; + const { + project + }: { + project: Models.Project; + } = $props(); + + let authMembershipsMfa = $state(project?.authMembershipsMfa ?? true); + let authMembershipsUserName = $state(project?.authMembershipsUserName ?? true); + let authMembershipsUserEmail = $state(project?.authMembershipsUserEmail ?? true); async function updateMembershipsPrivacy() { try { await sdk.forConsole.projects.updateMembershipsPrivacy({ - projectId: $project.$id, + projectId: project.$id, userName: authMembershipsUserName, userEmail: authMembershipsUserEmail, mfa: authMembershipsMfa @@ -60,9 +66,9 @@ diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte index 515fb2228b..88c2e91fdf 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte @@ -1,11 +1,9 @@ @@ -137,7 +147,9 @@ secondary fullWidth external={isSelfHosted} - href={isCloud ? $upgradeURL : 'https://cloud.appwrite.io/register'} + href={isCloud + ? getChangePlanUrl(project.teamId) + : 'https://cloud.appwrite.io/register'} on:click={() => { trackEvent(Click.CloudSignupClick, { from: 'button', diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte index 807adba3fe..de62ad3845 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte @@ -7,11 +7,15 @@ import { createTimeUnitPair } from '$lib/helpers/unit'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { project as projectStore } from '../../store'; import { Layout } from '@appwrite.io/pink-svelte'; - import { page } from '$app/state'; + import type { Models } from '@appwrite.io/console'; + + let { + project + }: { + project: Models.Project; + } = $props(); - const project = $derived($projectStore ?? page.data?.project); const { value, unit, baseValue, units } = $derived(createTimeUnitPair(project?.authDuration)); const options = $derived(units.map((v) => ({ label: v.name, value: v.name }))); diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte index eb0bb97c89..1c6f0bb904 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte @@ -7,14 +7,20 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { Typography } from '@appwrite.io/pink-svelte'; - import { project } from '../../store'; + import type { Models } from '@appwrite.io/console'; - let maxSessions = $project?.authSessionsLimit; + let { + project + }: { + project: Models.Project; + } = $props(); + + let maxSessions = $state(project?.authSessionsLimit); async function updateSessionsLimit() { try { await sdk.forConsole.projects.updateAuthSessionsLimit({ - projectId: $project.$id, + projectId: project.$id, limit: maxSessions }); await invalidate(Dependencies.PROJECT); @@ -49,7 +55,7 @@ bind:value={maxSessions} /> - + diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte index a319a1a08d..5296362cb2 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte @@ -7,29 +7,32 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { Layout, Selector, Input, Badge } from '@appwrite.io/pink-svelte'; - import { project } from '../../store'; import { tick } from 'svelte'; + import type { Models } from '@appwrite.io/console'; - let value = $project?.authLimit !== 0 ? 'limited' : 'unlimited'; + let { + project + }: { + project: Models.Project; + } = $props(); - $: isLimited = value === 'limited'; - let newLimit = isLimited ? $project?.authLimit : 100; + let maxUsersInputField: HTMLInputElement | null = $state(null); - $: btnDisabled = (function isBtnDisabled() { - if ( - (!isLimited && $project?.authLimit === 0) || - (isLimited && $project?.authLimit === newLimit) - ) { - return true; - } + let value = $state(project?.authLimit !== 0 ? 'limited' : 'unlimited'); + let newLimit = $state(project?.authLimit !== 0 ? project?.authLimit : 100); - return false; - })(); + const isLimited = $derived(value === 'limited'); + const btnDisabled = $derived.by(() => { + return ( + (!isLimited && project?.authLimit === 0) || + (isLimited && project?.authLimit === newLimit) + ); + }); async function updateLimit() { try { await sdk.forConsole.projects.updateAuthLimit({ - projectId: $project?.$id, + projectId: project?.$id, limit: isLimited ? newLimit : 0 }); await invalidate(Dependencies.PROJECT); @@ -47,13 +50,13 @@ } } - let maxUsersInputField: HTMLInputElement | null = null; - - $: if (isLimited && maxUsersInputField) { - tick().then(() => { - maxUsersInputField.focus(); - }); - } + $effect(() => { + if (isLimited && maxUsersInputField) { + tick().then(() => { + maxUsersInputField.focus(); + }); + } + }); diff --git a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte index 28a3896a12..5850c6ce73 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte @@ -1,5 +1,4 @@ -{#if $authMethods && $project} +{#if $authMethods && project} @@ -73,7 +75,7 @@ OAuth2 Providers
      - {#each $project.oAuthProviders + {#each project.oAuthProviders .filter((p) => p.name !== 'Mock') .sort( (a, b) => (a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1) ) as provider} {@const oAuthProvider = oAuthProviders[provider.key]} @@ -116,9 +118,9 @@ {/if} {#if selectedProvider && showProvider} - {@const oAuthProvider = oAuthProviders[selectedProvider.key]} - { diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/+page.svelte index da7178906c..72002ae96b 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/+page.svelte @@ -1,4 +1,4 @@ - @@ -54,7 +61,7 @@ class="u-margin-block-start-32" secondary fullWidth - href={$upgradeURL} + href={getChangePlanUrl(project.teamId)} on:click={() => { trackEvent(Click.OrganizationClickUpgrade, { from: 'button', diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte index 0e5757047e..cd5bb6958e 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailTemplate.svelte @@ -2,21 +2,37 @@ import { Button, Form, InputEmail, InputText, InputTextarea } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { project } from '../../store'; import ResetEmail from './resetEmail.svelte'; import { baseEmailTemplate, emailTemplate } from './store'; import deepEqual from 'deep-equal'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import type { EmailTemplateLocale, EmailTemplateType } from '@appwrite.io/console'; + import { + type EmailTemplateLocale, + type EmailTemplateType, + type Models + } from '@appwrite.io/console'; import { Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import TemplateSkeleton from './templateSkeleton.svelte'; + import type { Snippet } from 'svelte'; - export let loading = false; - export let isUpdating = false; + let { + loading = false, + isUpdating = false, + project, + children = null + }: { + loading: boolean; + isUpdating: boolean; + project: Models.Project; + children: Snippet; + } = $props(); - let openResetModal = false; - let eventType = Submit.EmailUpdateInviteTemplate; + let openResetModal = $state(false); + let eventType = $state(Submit.EmailUpdateInviteTemplate); + + const isSmtpEnabled = $derived(project?.smtpEnabled); + const isButtonDisabled = $derived(deepEqual($emailTemplate, $baseEmailTemplate)); async function saveEmailTemplate() { if (!$emailTemplate.locale) { @@ -51,7 +67,7 @@ } // TODO: fix TemplateType and TemplateLocale typing once SDK is updated await sdk.forConsole.projects.updateEmailTemplate({ - projectId: $project.$id, + projectId: project.$id, type: $emailTemplate.type as EmailTemplateType, locale: $emailTemplate.locale as EmailTemplateLocale, subject: $emailTemplate.subject || undefined, @@ -80,9 +96,6 @@ }); } } - - $: isSmtpEnabled = $project?.smtpEnabled; - $: isButtonDisabled = deepEqual($emailTemplate, $baseEmailTemplate);
      @@ -111,7 +124,7 @@ label="Reply to" placeholder="noreply@appwrite.io" /> - {#if $$slots.default} + {#if children}

      Click to copy variables for the fields below. Learn more here.

      + - + {@render children()} {/if} @@ -154,4 +168,4 @@
      - + diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte index 54f257fe4d..4f5f9d9532 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/emailVerificationTemplate.svelte @@ -3,18 +3,18 @@ import LocaleOptions from './localeOptions.svelte'; import { loadEmailTemplate } from './+page.svelte'; import { baseEmailTemplate, emailTemplate } from './store'; - import { page } from '$app/state'; import { Id } from '$lib/components'; import { addNotification } from '$lib/stores/notifications'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { Layout, Card } from '@appwrite.io/pink-svelte'; - import { EmailTemplateLocale, EmailTemplateType } from '@appwrite.io/console'; + import { EmailTemplateLocale, EmailTemplateType, type Models } from '@appwrite.io/console'; export let loading = false; + export let project: Models.Project; + export let localeCodes: Models.LocaleCode[]; let isUpdating = false; let locale = EmailTemplateLocale.En; - const projectId = page.params.project; async function onLocaleChange() { const timeout = setTimeout(() => { @@ -22,7 +22,7 @@ }, 1000); try { const template = await loadEmailTemplate( - projectId, + project.$id, EmailTemplateType.Verification, locale ); @@ -44,8 +44,8 @@ - - + + {'{{user}}'} {'{{project}}'} {'{{redirect}}'} diff --git a/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte b/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte index 4c4c334caf..08613e1591 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte @@ -1,10 +1,11 @@ @@ -20,5 +23,5 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/dangerZone.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/dangerZone.svelte index 30ecb4d7a5..1518c2e57b 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/dangerZone.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/dangerZone.svelte @@ -1,24 +1,31 @@ @@ -52,7 +59,7 @@ ? [$user.email, $user.phone].join(',') : $user.email || $user.phone}

      -

      Last activity: {accessedAt ? toLocaleDate(accessedAt) : 'never'}

      +

      Last activity: {$user.accessedAt ? toLocaleDate($user.accessedAt) : 'never'}

      @@ -61,4 +68,4 @@
      - + diff --git a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/deleteUser.svelte b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/deleteUser.svelte index 043cd64ac5..ffd2d48547 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/user-[user]/deleteUser.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/user-[user]/deleteUser.svelte @@ -1,29 +1,35 @@ @@ -37,4 +48,4 @@ {/key} - +{@render children()} diff --git a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte index 08f06a6ad0..692098fb3d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte @@ -25,9 +25,7 @@ let showCreate = $state(false); - const isLimited = $derived( - isServiceLimited('databases', $organization?.billingPlan, data.databases.total) - ); + const isLimited = $derived(isServiceLimited('databases', $organization, data.databases.total)); async function handleCreate(event: CustomEvent) { showCreate = false; @@ -113,4 +111,4 @@ {/if} - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/+page.ts index 7b823fa466..642f277e3d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/+page.ts @@ -6,7 +6,6 @@ import { timeFromNow } from '$lib/helpers/date'; import type { PageLoad, RouteParams } from './$types'; import { isSelfHosted } from '$lib/system'; import { isCloud } from '$lib/system'; -import type { Plan } from '$lib/sdk/billing'; import { useDatabaseSdk } from '$database/(entity)'; export const load: PageLoad = async ({ url, route, depends, params, parent }) => { @@ -46,7 +45,7 @@ async function fetchDatabasesAndBackups( offset: number, params: RouteParams, search?: string | undefined, - currentPlan?: Plan + currentPlan?: Models.BillingPlan ) { const backupsEnabled = currentPlan?.backupsEnabled ?? true; diff --git a/src/routes/(console)/project-[region]-[project]/databases/create.svelte b/src/routes/(console)/project-[region]-[project]/databases/create.svelte index 3fc1736ddc..a48ef0d1f6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/create.svelte @@ -4,11 +4,11 @@ import { Button, InputText } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { ID } from '@appwrite.io/console'; + import { ID, type Models } from '@appwrite.io/console'; import { createEventDispatcher } from 'svelte'; import { isCloud } from '$lib/system'; import { currentPlan } from '$lib/stores/organization'; - import { upgradeURL } from '$lib/stores/billing'; + import { getChangePlanUrl } from '$lib/stores/billing'; import CreatePolicy from './database-[database]/backups/createPolicy.svelte'; import { cronExpression, type UserBackupPolicy } from '$lib/helpers/backups'; import { Alert, Icon, Tag } from '@appwrite.io/pink-svelte'; @@ -16,6 +16,8 @@ import { page } from '$app/state'; export let showCreate = false; + export let project: Models.Project; + let totalPolicies: UserBackupPolicy[] = []; const dispatch = createEventDispatcher(); @@ -133,11 +135,12 @@ Upgrade your plan to ensure your data stays safe and backed up. - + {:else} { $showCreateBackup = true; @@ -227,7 +229,7 @@
    {:else}
    - +
    {/if}
    @@ -238,7 +240,11 @@ onSubmit={createPolicies} bind:show={$showCreatePolicy} bind:error={policyCreateError}> - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/containerHeader.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/containerHeader.svelte index 1ee5a6e2f1..11e2c3fa85 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/containerHeader.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/containerHeader.svelte @@ -1,13 +1,15 @@
    { showDropdown = !showDropdown; - goto($upgradeURL); + goto(getChangePlanUrl(project.teamId)); }}>Upgrade your plan to add customized backup policies. diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/createPolicy.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/createPolicy.svelte index 989a48b757..b6950baf4a 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/createPolicy.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/createPolicy.svelte @@ -10,7 +10,7 @@ InputTime, Label } from '$lib/elements/forms'; - import { ID } from '@appwrite.io/console'; + import { ID, type Models } from '@appwrite.io/console'; import { capitalize } from '$lib/helpers/string'; import { backupRetainingOptions, customRetainingOptions } from '../store'; import { presetPolicies, showCreatePolicy } from './store'; @@ -25,12 +25,13 @@ import { IconPencil, IconTrash } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport } from '$lib/stores/viewport'; import { goto } from '$app/navigation'; - import { upgradeURL } from '$lib/stores/billing'; + import { getChangePlanUrl } from '$lib/stores/billing'; export let isShowing: boolean; export let isFromBackupsTab: boolean = false; export let title: string | undefined = undefined; export let subtitle: string | undefined = undefined; + export let project: Models.Project; export let totalPolicies: UserBackupPolicy[] = []; @@ -213,7 +214,7 @@ on:click={() => { isShowing = false; $showCreatePolicy = false; - goto($upgradeURL); + goto(getChangePlanUrl(project.teamId)); }}>Upgrade your plan to add customized backup policies. @@ -229,7 +230,7 @@ { isShowing = false; - goto($upgradeURL); + goto(getChangePlanUrl(project.teamId)); }}>Upgrade your plan to add customized backup policies. diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/locked.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/locked.svelte index 24b41fe639..c456184aac 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/locked.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/locked.svelte @@ -11,19 +11,27 @@ import LockedBackupsDarkTablet from '$lib/images/backups/empty/backups-tablet-dark.png'; import LockedBackupsLightTablet from '$lib/images/backups/empty/backups-tablet-light.png'; + import type { Models } from '@appwrite.io/console'; + + let { + project + }: { + project: Models.Project; + } = $props();
    - +
    + buttonDisabled />
    + buttonDisabled />
    + buttonDisabled />
    + buttonDisabled />
    diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/upgradeCard.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/upgradeCard.svelte index 6fbfd92623..9b6aef824d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/upgradeCard.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/upgradeCard.svelte @@ -1,6 +1,6 @@ - - + +
    + +
    + + You have reached the maximum number of functions for your plan. + +
    {#if data.functions.total} @@ -95,11 +121,16 @@ event="functions" total={data.functions.total} service="functions" - on:click={() => - goto(`${base}/project-${page.params.region}-${project}/functions/create-function`)}> + on:click={() => goto(createFunctionsUrl)}> {#each data.functions.functions as func (func.$id)} + href={resolveRoute( + '/(console)/project-[region]-[project]/functions/function-[function]', + { + ...page.params, + function: func.$id + } + )}> @@ -135,7 +166,9 @@ @@ -145,9 +178,6 @@ allowCreate={$canWriteFunctions} href="https://appwrite.io/docs/products/functions" target="function" - on:click={() => - goto( - `${base}/project-${page.params.region}-${project}/functions/create-function` - )} /> + on:click={() => goto(createFunctionsUrl)} /> {/if}
    diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte index 9b29e6da15..ac39b30fac 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte @@ -1,9 +1,8 @@ @@ -39,10 +48,6 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateResourceLimits.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateResourceLimits.svelte index 30dcd26ff7..fce7e945d7 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateResourceLimits.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/settings/updateResourceLimits.svelte @@ -2,7 +2,7 @@ import { invalidate } from '$app/navigation'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { CardGrid } from '$lib/components'; - import { BillingPlan, Dependencies } from '$lib/constants'; + import { Dependencies } from '$lib/constants'; import { Button, Form, InputSelect } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; @@ -10,13 +10,14 @@ import { Runtime, type Models } from '@appwrite.io/console'; import Link from '$lib/elements/link.svelte'; import { Alert } from '@appwrite.io/pink-svelte'; - import { upgradeURL } from '$lib/stores/billing'; + import { isStarterPlan, getChangePlanUrl } from '$lib/stores/billing'; import { isCloud } from '$lib/system'; import { organization } from '$lib/stores/organization'; import { page } from '$app/state'; export let func: Models.Function; export let specs: Models.SpecificationList; + let specification = func.specification; let originalSpecification = func.specification; $: originalSpecification = func.specification; @@ -89,11 +90,14 @@ disabled={options.length < 1} bind:value={specification} {options} /> - {#if isCloud && $organization.billingPlan === BillingPlan.FREE} + + + {@const isStarter = isStarterPlan($organization.billingPlanId)} + {#if isCloud && isStarter} - Upgrade to Pro or Scale to adjust your CPU and RAM beyond the default. + Upgrade your plan to adjust your CPU and RAM beyond the default. - + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/functions/templates/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/templates/+page.svelte index de53032daf..2ddb9f30d6 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/templates/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/templates/+page.svelte @@ -79,11 +79,7 @@ .some((param) => param.toLowerCase() === useCase.toLowerCase()); }; - $: buttonDisabled = isServiceLimited( - 'functions', - $organization?.billingPlan, - $functionsList?.total ?? 0 - ); + $: buttonDisabled = isServiceLimited('functions', $organization, $functionsList?.total ?? 0); @@ -153,7 +149,7 @@ {:else} - {#snippet children(paginatedItems: typeof data.templates)} + {#snippet children(paginatedItems)} {#each paginatedItems as template} {@const baseRuntimes = getBaseRuntimes(template.runtimes)} diff --git a/src/routes/(console)/project-[region]-[project]/functions/templates/template-[template]/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/templates/template-[template]/+page.svelte index 4f5ca68a10..96aa113950 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/templates/template-[template]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/templates/template-[template]/+page.svelte @@ -26,7 +26,7 @@ import { IconExternalLink } from '@appwrite.io/pink-icons-svelte'; $: buttonDisabled = - isCloud && isServiceLimited('functions', $organization?.billingPlan, $functionsList?.total); + isCloud && isServiceLimited('functions', $organization, $functionsList?.total); diff --git a/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte b/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte index 0f3c96b173..2337428546 100644 --- a/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte +++ b/src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte @@ -1,7 +1,7 @@ - {#if $canWritePlatforms} - - {#if $canWritePlatforms} - - {/if} -
    - - addPlatform(Platform.Web)} - leadingIcon={IconCode}> - Web - - addPlatform(Platform.Flutter)} - leadingIcon={IconFlutter}> - Flutter - - addPlatform(Platform.Android)} - leadingIcon={IconAndroid}> - Android - - addPlatform(Platform.Apple)} - leadingIcon={IconApple}> - Apple - - addPlatform(Platform.ReactNative)} - leadingIcon={IconReact}> - React Native - - -
    -
    + {#if isLimited} + +
    + +
    + + + You have reached the maximum number of platforms for your plan in a project. + +
    + {:else} + + {#if $canWritePlatforms} + + {/if} +
    + + addPlatform(Platform.Web)} + leadingIcon={IconCode}> + Web + + addPlatform(Platform.Flutter)} + leadingIcon={IconFlutter}> + Flutter + + addPlatform(Platform.Android)} + leadingIcon={IconAndroid}> + Android + + addPlatform(Platform.Apple)} + leadingIcon={IconApple}> + Apple + + addPlatform(Platform.ReactNative)} + leadingIcon={IconReact}> + React Native + + +
    +
    + {/if} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte index 8c427cc23f..8e37b72f9b 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/+page.svelte @@ -94,8 +94,9 @@ {sdkCreateVariable} {sdkUpdateVariable} {sdkDeleteVariable} - isGlobal={true} + isGlobal variableList={data.variables} + project={data.project} analyticsSource="project_settings" /> diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte index 38def3870d..99e62c32f7 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte @@ -18,7 +18,6 @@ const dispatch = createEventDispatcher(); - // TODO: @itznotabug - check what needs to be changed here. we might need to do dual stuff to manage backwards compat? const labelMap = { users: { root: 'Users', teams: 'Include teams' }, databases: { root: 'Databases', rows: 'Include rows' }, diff --git a/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte b/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte index 24e9db8047..64dc9cb1e0 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/migrations/exportModal.svelte @@ -65,7 +65,7 @@ page.url.href, $user.name, $user.email, - $organization?.billingPlan, + $organization?.billingPlanId, $feedbackData.value, $organization?.$id, $project?.$id, diff --git a/src/routes/(console)/project-[region]-[project]/settings/smtp/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/smtp/+page.svelte index 322525df39..752c01aac2 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/smtp/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/smtp/+page.svelte @@ -2,32 +2,40 @@ import { CardGrid } from '$lib/components'; import { Button, Form, InputText, InputEmail } from '$lib/elements/forms'; import { Container } from '$lib/layout'; - import { project } from '../../store'; import InputPassword from '$lib/elements/forms/inputPassword.svelte'; import { sdk } from '$lib/stores/sdk'; import { invalidate } from '$app/navigation'; - import { BillingPlan, Dependencies } from '$lib/constants'; + import { Dependencies } from '$lib/constants'; import { addNotification } from '$lib/stores/notifications'; import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics'; import InputNumber from '$lib/elements/forms/inputNumber.svelte'; - import { base } from '$app/paths'; + import { resolve } from '$app/paths'; import deepEqual from 'deep-equal'; - import { onMount } from 'svelte'; - import { organization } from '$lib/stores/organization'; + import { currentPlan } from '$lib/stores/organization'; import { type SMTPSecure } from '@appwrite.io/console'; import InputSelect from '$lib/elements/forms/inputSelect.svelte'; - import { upgradeURL } from '$lib/stores/billing'; + import { getChangePlanUrl } from '$lib/stores/billing'; import { Link, Selector, Alert } from '@appwrite.io/pink-svelte'; + import type { PageProps } from './$types'; + import { isCloud } from '$lib/system'; - let enabled = false; - let senderName: string; - let senderEmail: string; - let replyTo: string; - let host: string; - let port: number; - let username: string; - let password: string; - let secure: string; + const { data }: PageProps = $props(); + + const { project } = data; + + let enabled: boolean = $state(false); + + let replyTo: string = $state(''); + let senderName: string = $state(''); + let senderEmail: string = $state(''); + + let host: string = $state(''); + let port: number = $state(null); + + let username: string = $state(''); + let password: string = $state(''); + + let secure: string = $state(''); const options = [ { value: 'tls', label: 'TLS' }, @@ -35,33 +43,37 @@ { value: '', label: 'None' } ]; - onMount(() => { - enabled = $project.smtpEnabled ?? false; - senderName = $project.smtpSenderName; - senderEmail = $project.smtpSenderEmail; - replyTo = $project.smtpReplyTo; - host = $project.smtpHost; - port = $project.smtpPort; - username = $project.smtpUsername; - password = $project.smtpPassword; - secure = $project.smtpSecure === 'tls' ? 'tls' : $project.smtpSecure === 'ssl' ? 'ssl' : ''; + const isButtonDisabled = $derived.by(() => { + return deepEqual( + { + enabled, + senderName, + senderEmail, + replyTo, + host, + port: port ?? '', + username, + password, + secure + }, + { + enabled: project.smtpEnabled, + senderName: project.smtpSenderName, + senderEmail: project.smtpSenderEmail, + replyTo: project.smtpReplyTo, + host: project.smtpHost, + port: project.smtpPort, + username: project.smtpUsername, + password: project.smtpPassword, + secure: project.smtpSecure + } + ); }); async function updateSmtp() { try { - if (!enabled) { - enabled = false; - senderName = undefined; - senderEmail = undefined; - replyTo = undefined; - host = undefined; - port = undefined; - username = undefined; - password = undefined; - } - await sdk.forConsole.projects.updateSMTP({ - projectId: $project.$id, + projectId: project.$id, enabled, senderName: senderName || undefined, senderEmail: senderEmail || undefined, @@ -76,7 +88,7 @@ invalidate(Dependencies.PROJECT); addNotification({ type: 'success', - message: 'SMTP server has been ' + (enabled ? 'enabled.' : 'disabled.') + message: `SMTP server has been ${enabled ? 'enabled.' : 'disabled.'}` }); trackEvent(Submit.ProjectUpdateSMTP); } catch (error) { @@ -88,31 +100,30 @@ } } - $: isButtonDisabled = deepEqual( - { enabled, senderName, senderEmail, replyTo, host, port, username, password, secure }, - { - enabled: $project.smtpEnabled, - senderName: $project.smtpSenderName, - senderEmail: $project.smtpSenderEmail, - replyTo: $project.smtpReplyTo, - host: $project.smtpHost, - port: $project.smtpPort, - username: $project.smtpUsername, - password: $project.smtpPassword, - secure: $project.smtpSecure - } - ); + $effect(() => { + enabled = project.smtpEnabled ?? false; + senderName = project.smtpSenderName; + senderEmail = project.smtpSenderEmail; + replyTo = project.smtpReplyTo; + host = project.smtpHost; + port = project.smtpPort; + username = project.smtpUsername; + password = project.smtpPassword; + secure = project.smtpSecure === 'tls' ? 'tls' : project.smtpSecure === 'ssl' ? 'ssl' : ''; + }); - $: if (!enabled) { - senderName = undefined; - senderEmail = undefined; - replyTo = undefined; - host = undefined; - port = undefined; - username = undefined; - password = undefined; - secure = undefined; - } + $effect(() => { + if (!enabled) { + senderName = ''; + senderEmail = ''; + replyTo = ''; + host = ''; + port = null; + username = ''; + password = ''; + secure = ''; + } + }); @@ -120,17 +131,20 @@ SMTP server You can customize the email service by providing your own SMTP server. View your email templates - here + here - {#if $organization.billingPlan === BillingPlan.FREE} + {#if isCloud && !$currentPlan.customSmtp} + title="Custom SMTP is a paid plan feature. Upgrade to enable custom SMTP server."> diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte index 15ee7b648c..521d08acda 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte @@ -1,20 +1,25 @@
    Usage - {#if $organization?.billingPlan === BillingPlan.FREE} - {/if}
    +
    - {#if $organization.billingPlan === BillingPlan.SCALE} + {#if planHasGroup(currentBillingPlan, BillingPlanGroup.Scale)}

    On the Scale plan, you'll be charged only for any usage that exceeds the thresholds per resource listed below. ($showUsageRatesModal = true)} >Learn more about plan usage limits.

    - {:else if $organization.billingPlan === BillingPlan.PRO} + {:else if planHasGroup(currentBillingPlan, BillingPlanGroup.Pro)}

    On the Pro plan, you'll be charged only for any usage that exceeds the thresholds per resource listed below. ($showUsageRatesModal = true)} >Learn more about plan usage limits.

    - {:else if $organization.billingPlan === BillingPlan.FREE} + {:else if planHasGroup(currentBillingPlan, BillingPlanGroup.Starter)}

    - If you exceed the limits of the {plan} plan, services for your projects may be disrupted. - Upgrade for greater capacity.

    diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts index e5e1ee5a6c..0efb7c6020 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts @@ -1,53 +1,59 @@ -import type { AggregationTeam, Invoice, InvoiceUsage } from '$lib/sdk/billing'; -import { accumulateUsage } from '$lib/sdk/usage'; import { sdk } from '$lib/stores/sdk'; -import { Query } from '@appwrite.io/console'; import type { PageLoad } from './$types'; +import { accumulateUsage } from '$lib/sdk/usage'; +import { type Models, Query } from '@appwrite.io/console'; export const load: PageLoad = async ({ params, parent }) => { - const { invoice, project, region } = params; + const { invoice: invoiceId, project, region } = params; const { organization } = await parent(); let startDate: string = organization.billingCurrentInvoiceDate; let endDate: string = organization.billingNextInvoiceDate; - let currentInvoice: Invoice = undefined; - let currentAggregation: AggregationTeam = undefined; + let currentInvoice: Models.Invoice = undefined; + let currentAggregation: Models.AggregationTeam = undefined; - if (invoice) { - currentInvoice = await sdk.forConsole.billing.getInvoice(organization.$id, invoice); - currentAggregation = await sdk.forConsole.billing.getAggregation( - organization.$id, - currentInvoice.aggregationId - ); + if (invoiceId) { + currentInvoice = await sdk.forConsole.organizations.getInvoice({ + organizationId: organization.$id, + invoiceId + }); + currentAggregation = await sdk.forConsole.organizations.getAggregation({ + organizationId: organization.$id, + aggregationId: currentInvoice.aggregationId + }); startDate = currentInvoice.from; endDate = currentInvoice.to; } else { try { - currentAggregation = await sdk.forConsole.billing.getAggregation( - organization.$id, - organization.billingAggregationId - ); + currentAggregation = await sdk.forConsole.organizations.getAggregation({ + organizationId: organization.$id, + aggregationId: organization.billingAggregationId + }); } catch (e) { // ignore error if no aggregation found } } const [invoices, usage] = await Promise.all([ - sdk.forConsole.billing.listInvoices(organization.$id, [Query.orderDesc('from')]), + sdk.forConsole.organizations.listInvoices({ + organizationId: organization.$id, + queries: [Query.orderDesc('from')] + }), sdk.forProject(region, project).project.getUsage({ startDate, endDate }) ]); if (currentAggregation) { - let projectSpecificData = null; + let projectSpecificData: Models.AggregationBreakdown | null = null; if (currentAggregation.breakdown) { projectSpecificData = currentAggregation.breakdown.find((p) => p.$id === project); } if (projectSpecificData) { const executionsResource = projectSpecificData.resources?.find?.( - (r: InvoiceUsage) => r.resourceId === 'executions' + (resource) => resource.resourceId === 'executions' ); + if (executionsResource) { usage.executionsTotal = executionsResource.value || usage.executionsTotal; } diff --git a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte index e7ec5e8d12..da34dceffc 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/webhooks/+page.svelte @@ -1,5 +1,5 @@ {#if $canWriteWebhooks} - + +
    + +
    + + You have reached the maximum number of webhooks for your plan. + +
    {/if}
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte index 3c4ee175a9..e9ffd7c38d 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte @@ -40,8 +40,7 @@ }, keys: ['c'], disabled: - isServiceLimited('sites', $organization?.billingPlan, data.siteList?.total) || - !$canWriteSites, + isServiceLimited('sites', $organization, data.siteList?.total) || !$canWriteSites, icon: IconPlus, group: 'sites' } diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte index 1d1437942d..dc5d0a1403 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte @@ -188,7 +188,7 @@ items={variables} limit={6} hideFooter={variables.length <= 6}> - {#snippet children(paginatedItems: typeof variables)} + {#snippet children(paginatedItems)} - {#snippet children(paginatedItems: typeof data.templates)} + {#snippet children(paginatedItems)} {#each paginatedItems as template (template.name)} {@const templateFrameworks = template.frameworks.map((t) => t.name)} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte index dadbd653b8..d6d4746cfd 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte @@ -128,7 +128,7 @@ (prodDeployment) => prodDeployment.$id !== deployment.$id )} - {#snippet children(paginatedItems: typeof items)} + {#snippet children(paginatedItems)} {#each paginatedItems as prodDeployment} {#if isCloud} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte index bc4dcea093..01a56b0d62 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte @@ -2,20 +2,21 @@ import { invalidate } from '$app/navigation'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { CardGrid } from '$lib/components'; - import { BillingPlan, Dependencies } from '$lib/constants'; + import { Dependencies } from '$lib/constants'; import { Button, Form, InputSelect } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { Adapter, BuildRuntime, Framework, type Models } from '@appwrite.io/console'; import Link from '$lib/elements/link.svelte'; import { Alert } from '@appwrite.io/pink-svelte'; - import { upgradeURL } from '$lib/stores/billing'; + import { getChangePlanUrl, isStarterPlan } from '$lib/stores/billing'; import { isCloud } from '$lib/system'; import { organization } from '$lib/stores/organization'; import { page } from '$app/state'; export let site: Models.Site; export let specs: Models.SpecificationList; + let specification = site.specification; let originalSpecification = site.specification; @@ -82,11 +83,14 @@ disabled={options.length < 1} bind:value={specification} {options} /> - {#if isCloud && $organization.billingPlan === BillingPlan.FREE} + + + {@const isStarter = isStarterPlan($organization.billingPlanId)} + {#if isCloud && isStarter} - Upgrade to Pro or Scale to adjust your CPU and RAM beyond the default. + Upgrade your plan to adjust your CPU and RAM beyond the default. - + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/storage/+page.svelte b/src/routes/(console)/project-[region]-[project]/storage/+page.svelte index 71ec0578c9..50463873e3 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/+page.svelte @@ -1,4 +1,4 @@ - @@ -7,28 +7,34 @@ import { Empty, PaginationWithLimit } from '$lib/components'; import Create from './create.svelte'; import { Container, ResponsiveContainerHeader } from '$lib/layout'; - import { base } from '$app/paths'; import { page } from '$app/state'; import type { Models } from '@appwrite.io/console'; import { writable } from 'svelte/store'; import { canWriteBuckets } from '$lib/stores/roles'; - import { Icon } from '@appwrite.io/pink-svelte'; + import { Icon, Tooltip } from '@appwrite.io/pink-svelte'; import { Button } from '$lib/elements/forms'; import { columns } from './store'; import Grid from './grid.svelte'; import Table from './table.svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; + import { resolveRoute } from '$lib/stores/navigation'; + import { isServiceLimited } from '$lib/stores/billing'; + import type { PageProps } from './$types'; + import { organization } from '$lib/stores/organization'; - export let data; - - const project = page.params.project; + let { data }: PageProps = $props(); async function bucketCreated(event: CustomEvent) { showCreateBucket.set(false); await goto( - `${base}/project-${page.params.region}-${project}/storage/bucket-${event.detail.$id}` + resolveRoute('/(console)/project-[region]-[project]/storage/bucket-[bucket]', { + ...page.params, + bucket: event.detail.$id + }) ); } + + const isLimited = $derived(isServiceLimited('buckets', $organization, data.buckets.total)); @@ -38,10 +44,21 @@ bind:view={data.view} searchPlaceholder="Search by name or ID"> {#if $canWriteBuckets} - + +
    + +
    + + You have reached the maximum number of buckets for your plan. + +
    {/if} diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte index f0b24c0605..dd7b874fa7 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte @@ -2,19 +2,17 @@ import { Click, Submit, trackEvent } from '$lib/actions/analytics'; import { CardGrid } from '$lib/components'; import { Alert } from '@appwrite.io/pink-svelte'; - import { BillingPlan } from '$lib/constants'; import { Button, Form, InputNumber, InputSelect } from '$lib/elements/forms'; import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion'; import { createByteUnitPair } from '$lib/helpers/unit'; - import { readOnly, upgradeURL } from '$lib/stores/billing'; + import { getChangePlanUrl, isStarterPlan, readOnly } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system'; import { updateBucket } from './+page.svelte'; - import type { Plan } from '$lib/sdk/billing'; - import type { Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; export let bucket: Models.Bucket; - export let currentPlan: Plan | null; + export let currentPlan: Models.BillingPlan | null; const service = currentPlan ? currentPlan['fileSize'] : null; const { value, unit, baseValue, units } = createByteUnitPair(bucket.maximumFileSize, 1000); @@ -46,7 +44,9 @@ {#if isCloud} {@const size = humanFileSize(sizeToBytes(service, 'MB', 1000))} - {#if $organization?.billingPlan === BillingPlan.FREE} + + {@const isStarter = isStarterPlan($organization.billingPlanId)} + {#if isStarter} The {currentPlan.name} plan has a maximum upload file size limit of {Math.floor( parseInt(size.value) @@ -55,7 +55,7 @@