diff --git a/package-lock.json b/package-lock.json index f9918446f..0e8f02c59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,8 @@ "name": "@appwrite/console", "dependencies": { "@appwrite.io/console": "^0.6.1", - "@appwrite.io/pink": "0.16.0", - "@appwrite.io/pink-icons": "0.16.0", + "@appwrite.io/pink": "0.17.0", + "@appwrite.io/pink-icons": "0.17.0", "@popperjs/core": "^2.11.8", "@sentry/svelte": "^7.66.0", "@sentry/tracing": "^7.66.0", @@ -166,19 +166,19 @@ } }, "node_modules/@appwrite.io/pink": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.16.0.tgz", - "integrity": "sha512-KprOd+36I0w6PC0x6NJclvyJNpCsjAPcrCHL3QixSX0dZ66wqFE6NitF/KiVVCxbuBaNgjrm7oQ0hr7dt1Sm5Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.17.0.tgz", + "integrity": "sha512-1FkVVqdqU5d0FlGIyLisCoLR3JaqGlQs3+BJydo6Mr9/bRWKcg4O9nw+e1P1Lx8StnVnDEM5CRCQdzyPQSNByw==", "dependencies": { - "@appwrite.io/pink-icons": "0.16.0", + "@appwrite.io/pink-icons": "0.17.0", "normalize.css": "^8.0.1", "the-new-css-reset": "^1.11.2" } }, "node_modules/@appwrite.io/pink-icons": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.16.0.tgz", - "integrity": "sha512-tWaG5lFXnKzbFbGf4SlxoFYj2yH7kQDejFq+UHdfzBrqteSfZpxIxdNmKHIea1ztcmsat4y+R0ZbVX9iOv+oPw==" + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.17.0.tgz", + "integrity": "sha512-JcYjVI+nPO8BTJzKlUH2RKyUYg/3pEy81dr3TrQB0HlsrngDq3akhaNCfnQzLg9Kicdl+Cr4Gq920Ha6zA4FQQ==" }, "node_modules/@babel/code-frame": { "version": "7.24.2", diff --git a/package.json b/package.json index e021b3c61..eb7eba70d 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "e2e": "playwright test tests/e2e" }, "dependencies": { - "@appwrite.io/pink": "0.16.0", - "@appwrite.io/pink-icons": "0.16.0", + "@appwrite.io/pink": "0.17.0", + "@appwrite.io/pink-icons": "0.17.0", "@appwrite.io/console": "^0.6.1", "@popperjs/core": "^2.11.8", "@sentry/svelte": "^7.66.0", diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte index a20d951b2..6ead40362 100644 --- a/src/lib/components/billing/estimatedTotalBox.svelte +++ b/src/lib/components/billing/estimatedTotalBox.svelte @@ -28,7 +28,7 @@ : grossCost; - +

{currentPlan.name} plan

{formatCurrency(currentPlan.price)}

diff --git a/src/lib/components/billing/paymentModal.svelte b/src/lib/components/billing/paymentModal.svelte index 2b5ef5d1b..56bcdab20 100644 --- a/src/lib/components/billing/paymentModal.svelte +++ b/src/lib/components/billing/paymentModal.svelte @@ -68,7 +68,12 @@ } - + - + diff --git a/src/lib/components/billing/planComparisonBox.svelte b/src/lib/components/billing/planComparisonBox.svelte index d9ae9326e..85443bcfb 100644 --- a/src/lib/components/billing/planComparisonBox.svelte +++ b/src/lib/components/billing/planComparisonBox.svelte @@ -25,25 +25,25 @@
{#if selectedTab === 'tier-0'}

{plan.name} plan

-
    -
  • +
      +
    • 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
    • +
    • Limited to 1 organization member
    • +
    • {plan.bandwidth}GB bandwidth
    • +
    • {plan.storage}GB storage
    • +
    • {formatNum(plan.executions)} executions
    {:else if selectedTab === 'tier-1'}

    {plan.name} plan

    -
      -
    • Everything in the Starter plan, plus:
    • -
    • Unlimited databases, buckets, functions
    • -
    • {plan.bandwidth}GB bandwidth
    • -
    • {plan.storage}GB storage
    • -
    • {formatNum(plan.executions)} executions
    • -
    • Email support
    • +
        +
      • Everything in the Starter plan, plus:
      • +
      • Unlimited databases, buckets, functions
      • +
      • {plan.bandwidth}GB bandwidth
      • +
      • {plan.storage}GB storage
      • +
      • {formatNum(plan.executions)} executions
      • +
      • Email support
      {/if}
diff --git a/src/lib/components/billing/selectPaymentMethod.svelte b/src/lib/components/billing/selectPaymentMethod.svelte index 3e4d9269c..fd3ed238f 100644 --- a/src/lib/components/billing/selectPaymentMethod.svelte +++ b/src/lib/components/billing/selectPaymentMethod.svelte @@ -73,7 +73,7 @@ {:else} - +

diff --git a/src/lib/constants.ts b/src/lib/constants.ts index fd4a80b82..3d994e792 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -350,3 +350,38 @@ export enum BillingPlan { PRO = 'tier-1', SCALE = 'tier-2' } + +export const feedbackDowngradeOptions = [ + { + value: 'availableFeatures', + label: "The available features don't meet my needs" + }, + { + value: 'traction', + label: "My project isn't getting traction" + }, + { + value: 'bugs', + label: 'I experienced bugs or unexpected outages while using the console' + }, + { + value: 'starter', + label: 'The Starter plan is enough for my projects' + }, + { + value: 'budget', + label: "I don't have the budget" + }, + { + value: 'tryOut', + label: 'I just wanted to try it out' + }, + { + value: 'alternative', + label: 'I found an alternative/competitor to meet my needs' + }, + { + value: 'other', + label: 'Other' + } +]; diff --git a/src/lib/layout/wizardSecondaryHeader.svelte b/src/lib/layout/wizardSecondaryHeader.svelte index e7971f533..91a5bda0e 100644 --- a/src/lib/layout/wizardSecondaryHeader.svelte +++ b/src/lib/layout/wizardSecondaryHeader.svelte @@ -5,8 +5,8 @@

-
- +
+ diff --git a/src/routes/console/organization-[organization]/change-plan/+page.svelte b/src/routes/console/organization-[organization]/change-plan/+page.svelte index 1f1064969..ca73b4ae6 100644 --- a/src/routes/console/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/console/organization-[organization]/change-plan/+page.svelte @@ -3,15 +3,23 @@ import { base } from '$app/paths'; import { page } from '$app/stores'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { LabelCard } from '$lib/components'; + import { Alert, LabelCard } from '$lib/components'; import { EstimatedTotalBox, PlanComparisonBox, SelectPaymentMethod } from '$lib/components/billing'; import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte'; - import { BillingPlan, Dependencies } from '$lib/constants'; - import { Button, Form, FormList, InputTags, Label } from '$lib/elements/forms'; + import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants'; + import { + Button, + Form, + FormList, + InputSelect, + InputTags, + InputTextarea, + Label + } from '$lib/elements/forms'; import { formatCurrency } from '$lib/helpers/numbers'; import { WizardSecondaryContainer, @@ -19,16 +27,23 @@ WizardSecondaryFooter, WizardSecondaryHeader } from '$lib/layout'; - import type { Coupon, PaymentList } from '$lib/sdk/billing'; + import { type Coupon, type PaymentList } from '$lib/sdk/billing'; import { plansInfo, tierFree, tierPro, tierToPlan, type Tier } from '$lib/stores/billing'; import { addNotification } from '$lib/stores/notifications'; - import { organization, organizationList, type Organization } from '$lib/stores/organization'; + import { + organization, + organizationList, + type Organization, + type OrganizationList + } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; import { user } from '$lib/stores/user'; import { VARS } from '$lib/system'; import { onMount } from 'svelte'; import { writable } from 'svelte/store'; + export let data; + $: anyOrgFree = $organizationList.teams?.find( (org) => (org as Organization)?.billingPlan === BillingPlan.STARTER ); @@ -41,21 +56,29 @@ let formComponent: Form; let isSubmitting = writable(false); - let methods: PaymentList; let billingPlan: Tier = $organization.billingPlan; let paymentMethodId: string; - let collaborators: string[] = []; + let collaborators: string[] = + data?.members?.memberships + ?.map((m) => { + if (m.userEmail !== $user.email) return m.userEmail; + }) + ?.filter(Boolean) ?? []; let couponData: Partial = { code: null, status: null, credits: null }; let taxId: string; - let billingBudget: number; let showCreditModal = false; + let feedbackDowngradeReason: string; + let feedbackMessage: string; + + let orgList: OrganizationList; + onMount(async () => { if ($page.url.searchParams.has('coupon')) { const coupon = $page.url.searchParams.get('coupon'); @@ -70,9 +93,6 @@ }; } } - if ($page.url.searchParams.has('name')) { - name = $page.url.searchParams.get('name'); - } if ($page.url.searchParams.has('plan')) { const plan = $page.url.searchParams.get('plan'); if (plan && plan in BillingPlan) { @@ -80,183 +100,163 @@ } } billingPlan = $organization.billingPlan; + + if (anyOrgFree && billingPlan === BillingPlan.PRO) { + orgList = await sdk.forConsole.billing.listOrganization(); + } }); async function loadPaymentMethods() { methods = await sdk.forConsole.billing.listPaymentMethods(); - paymentMethodId = methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null; + + paymentMethodId = + $organization?.paymentMethodId ?? + methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? + null; } - const feedbackDowngradeOptions = [ - { - value: 'availableFeatures', - label: "The available features don't meet my needs" - }, - { - value: 'traction', - label: "My project isn't getting traction" - }, - { - value: 'bugs', - label: 'I experienced bugs or unexpected outages while using the console' - }, - { - value: 'starter', - label: 'The Starter plan is enough for my projects' - }, - { - value: 'budget', - label: "I don't have the budget" - }, - { - value: 'tryOut', - label: 'I just wanted to try it out' - }, - { - value: 'alternative', - label: 'I found an alternative/competitor to meet my needs' - }, - { - value: 'other', - label: 'Other' - } - ]; - - let feedbackDowngradeReason: string; - let feedbackMessage: string; - - async function update() { - //Downgrade + async function handleSubmit() { if (billingPlan === BillingPlan.STARTER) { - try { - await sdk.forConsole.billing.updatePlan( - $organization.$id, - billingPlan, - paymentMethodId, - null + await downgrade(); + } else { + await upgrade(); + } + } + + async function downgrade() { + try { + await sdk.forConsole.billing.updatePlan( + $organization.$id, + billingPlan, + paymentMethodId, + null + ); + + await fetch(`${VARS.GROWTH_ENDPOINT}/feedback/billing`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + from: tierToPlan($organization.billingPlan).name, + to: tierToPlan(billingPlan).name, + email: $user.email, + reason: feedbackDowngradeOptions.find( + (option) => option.value === feedbackDowngradeReason + )?.label, + orgId: $organization.$id, + userId: $user.$id, + message: feedbackMessage ?? '' + }) + }); + + addNotification({ + type: 'success', + isHtml: true, + message: ` + ${$organization.name} has been changed to ${ + tierToPlan(billingPlan).name + } plan.` + }); + + trackEvent(Submit.OrganizationDowngrade, { + plan: tierToPlan(billingPlan)?.name + }); + } catch (e) { + addNotification({ + type: 'error', + message: e.message + }); + trackError(e, isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade); + } + } + + async function upgrade() { + try { + const org = await sdk.forConsole.billing.updatePlan( + $organization.$id, + billingPlan, + paymentMethodId, + null + ); + + //Add coupon + if (couponData?.code) { + await sdk.forConsole.billing.addCredit(org.$id, couponData.code); + trackEvent(Submit.CreditRedeem); + } + + //Add budget + if (billingBudget) { + await sdk.forConsole.billing.updateBudget(org.$id, billingBudget, [75]); + } + + //Add collaborators + if (collaborators?.length) { + const newCollaborators = collaborators.filter( + (collaborator) => + !data?.members?.memberships?.find((m) => m.userEmail === collaborator) ); - - await fetch(`${VARS.GROWTH_ENDPOINT}/feedback/billing`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - from: tierToPlan($organization.billingPlan).name, - to: tierToPlan(billingPlan).name, - email: $user.email, - reason: feedbackDowngradeOptions.find( - (option) => option.value === feedbackDowngradeReason - )?.label, - orgId: $organization.$id, - userId: $user.$id, - message: feedbackMessage ?? '' - }) + newCollaborators.forEach(async (collaborator) => { + await sdk.forConsole.teams.createMembership( + org.$id, + ['owner'], + collaborator, + undefined, + undefined, + `${$page.url.origin}/console/organization-${org.$id}` + ); }); + } + //Add tax ID + if (taxId) { + await sdk.forConsole.billing.updateTaxId(org.$id, taxId); + } + + await invalidate(Dependencies.ACCOUNT); + await invalidate(Dependencies.ORGANIZATION); + + await goto(`/console/organization-${org.$id}`); + if (isUpgrade) { + addNotification({ + type: 'success', + message: 'Your organization has been upgraded' + }); + } else { addNotification({ type: 'success', isHtml: true, message: ` - ${$organization.name} has been changed to ${ - tierToPlan(billingPlan).name - } plan.` - }); - - trackEvent(Submit.OrganizationDowngrade, { - plan: tierToPlan(billingPlan)?.name - }); - } catch (e) { - addNotification({ - type: 'error', - message: e.message - }); - trackError( - e, - isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade - ); - } - } else { - try { - const org = await sdk.forConsole.billing.updatePlan( - $organization.$id, - billingPlan, - paymentMethodId, - null - ); - - //Add coupon - if (couponData?.code) { - await sdk.forConsole.billing.addCredit(org.$id, couponData.code); - trackEvent(Submit.CreditRedeem); - } - - //Add budget - if (billingBudget) { - await sdk.forConsole.billing.updateBudget(org.$id, billingBudget, [75]); - } - - //Add collaborators - if (collaborators?.length) { - collaborators.forEach(async (collaborator) => { - await sdk.forConsole.teams.createMembership( - org.$id, - ['owner'], - collaborator, - undefined, - undefined, - `${$page.url.origin}/console/organization-${org.$id}` - ); - }); - } - - //Add tax ID - if (taxId) { - await sdk.forConsole.billing.updateTaxId(org.$id, taxId); - } - - await invalidate(Dependencies.ACCOUNT); - await invalidate(Dependencies.ORGANIZATION); - - await goto(`/console/organization-${org.$id}`); - if (isUpgrade) { - addNotification({ - type: 'success', - message: 'Your organization has been upgraded' - }); - } else { - addNotification({ - type: 'success', - isHtml: true, - message: ` ${$organization.name} will change to ${ tierToPlan(billingPlan).name } plan at the end of the current billing cycle.` - }); - } - trackEvent(isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade, { - plan: tierToPlan(billingPlan)?.name }); - } catch (e) { - addNotification({ - type: 'error', - message: e.message - }); - trackError( - e, - isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade - ); } + trackEvent(isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade, { + plan: tierToPlan(billingPlan)?.name + }); + } catch (e) { + addNotification({ + type: 'error', + message: e.message + }); + trackError(e, isUpgrade ? Submit.OrganizationUpgrade : Submit.OrganizationDowngrade); } } $: isUpgrade = billingPlan > $organization.billingPlan; - $: freePlan = $plansInfo.get(BillingPlan.STARTER); $: proPlan = $plansInfo.get(BillingPlan.PRO); $: if (billingPlan === BillingPlan.PRO) { loadPaymentMethods(); } + $: isButtonDisabled = + $organization.billingPlan === billingPlan + ? true + : isUpgrade + ? !paymentMethodId + : !feedbackDowngradeReason; @@ -266,12 +266,20 @@ Change plan -
+

For more details on our plans, visit our .

+ {#if anyOrgFree && billingPlan === BillingPlan.PRO} + + You are limited to one Starter organization per account. Consider upgrading or + deleting . + + {/if}
    @@ -346,6 +354,22 @@ {/if} {/if} + {#if !isUpgrade && billingPlan === BillingPlan.STARTER} + + + + + {/if} {#if billingPlan !== BillingPlan.STARTER} @@ -365,7 +389,7 @@ diff --git a/src/routes/console/organization-[organization]/change-plan/+page.ts b/src/routes/console/organization-[organization]/change-plan/+page.ts new file mode 100644 index 000000000..cde580678 --- /dev/null +++ b/src/routes/console/organization-[organization]/change-plan/+page.ts @@ -0,0 +1,10 @@ +import { Dependencies } from '$lib/constants'; + +export const load = async ({ depends, parent }) => { + const { members } = await parent(); + depends(Dependencies.ORGANIZATION); + + return { + members + }; +};