From eafb780f4c7064057a711975e7b76fe37640efeb Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 12 Mar 2025 20:36:48 +0400 Subject: [PATCH] fix: create org --- .../billing/estimatedTotalBox.svelte | 93 +++--- .../components/billing/planSelection.svelte | 36 ++- .../billing/selectPaymentMethod.svelte | 1 - .../create-organization/+page.svelte | 169 ++++------ .../(console)/create-organization/+page.ts | 46 +++ .../billing/budgetCap.svelte | 39 ++- .../billing/paymentHistory.svelte | 6 +- .../change-plan/+page.svelte | 300 ++++++++---------- .../change-plan/+page.ts | 47 ++- 9 files changed, 362 insertions(+), 375 deletions(-) create mode 100644 src/routes/(console)/create-organization/+page.ts diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte index 474ed59f8..5fa7bee1d 100644 --- a/src/lib/components/billing/estimatedTotalBox.svelte +++ b/src/lib/components/billing/estimatedTotalBox.svelte @@ -1,14 +1,16 @@ -
- - -

{currentPlan.name} plan

-

{formatCurrency(currentPlan.price)}

-
- -

Additional seats ({collaborators?.length})

-

- {formatCurrency(extraSeatsCost)} -

-
- {#if couponData?.status === 'active'} - - {/if} -
- -

- Upcoming charge
Due on {!currentPlan.trialDays - ? toLocaleDate(billingPayDate.toString()) - : toLocaleDate(trialEndDate.toString())} -

-

- {formatCurrency(estimatedTotal)} -

-
- -

- You'll pay {formatCurrency(estimatedTotal)} now, with your first - billing cycle starting on - {!currentPlan.trialDays - ? toLocaleDate(billingPayDate.toString()) - : toLocaleDate(trialEndDate.toString())}. {#if couponData?.status === 'active'}Once your credits run out, you'll be charged - {formatCurrency(currentPlan.price)} plus usage fees every 30 - days. + + + + + {currentPlan.name} plan + {formatCurrency(currentPlan.price)} + + + Additional seats ({collaborators?.length}) + {formatCurrency(extraSeatsCost)} + + {#if couponData?.status === 'active'} + {/if} -

+ + + + Upcoming charge
+ Due on {!currentPlan.trialDays + ? toLocaleDate(billingPayDate.toString()) + : toLocaleDate(trialEndDate.toString())}
+ {formatCurrency(estimatedTotal)} +
+ + + You'll pay {formatCurrency(estimatedTotal)} now, with your first billing cycle + starting on + {!currentPlan.trialDays + ? toLocaleDate(billingPayDate.toString()) + : toLocaleDate(trialEndDate.toString())}. {#if couponData?.status === 'active'}Once your credits run out, you'll be charged + {formatCurrency(currentPlan.price)} plus usage fees every 30 days. + {/if} + - {/if} - -
+ + diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index f016c6871..55e137572 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -74,23 +74,25 @@
  • - - -
    -

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

    -

    - {tierScale.description} -

    -

    - {formatCurrency(scalePlan?.price ?? 0)} per month + usage -

    -
    -
    + +
    +

    + {#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg} + Current plan + {/if} +

    +

    + {tierScale.description} +

    +

    + {formatCurrency(scalePlan?.price ?? 0)} per month + usage +

    +
  • diff --git a/src/lib/components/billing/selectPaymentMethod.svelte b/src/lib/components/billing/selectPaymentMethod.svelte index cc479938d..c81513822 100644 --- a/src/lib/components/billing/selectPaymentMethod.svelte +++ b/src/lib/components/billing/selectPaymentMethod.svelte @@ -41,7 +41,6 @@ }); $: filteredMethods = methods?.paymentMethods?.filter((method) => !!method?.last4); - $: selectedPaymentMethod = methods?.paymentMethods?.find((method) => method.$id === value); diff --git a/src/routes/(console)/create-organization/+page.svelte b/src/routes/(console)/create-organization/+page.svelte index 5155ed62b..0e9d1a781 100644 --- a/src/routes/(console)/create-organization/+page.svelte +++ b/src/routes/(console)/create-organization/+page.svelte @@ -10,77 +10,37 @@ SelectPaymentMethod } from '$lib/components/billing'; import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte'; - import Default from '$lib/components/roles/default.svelte'; import { BillingPlan, Dependencies } from '$lib/constants'; - import { Button, Form, FormList, InputTags, InputText, Label } from '$lib/elements/forms'; - import { - WizardSecondaryContainer, - WizardSecondaryContent, - WizardSecondaryFooter - } from '$lib/layout'; + import { Button, Form, InputTags, InputText } from '$lib/elements/forms'; + import { Wizard } from '$lib/layout'; import type { Coupon, PaymentList } from '$lib/sdk/billing'; import { tierToPlan } from '$lib/stores/billing'; import { addNotification } from '$lib/stores/notifications'; - import { organizationList, type Organization } from '$lib/stores/organization'; + import type { Organization } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; import { ID } from '@appwrite.io/console'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; - import { Icon } from '@appwrite.io/pink-svelte'; - import { onMount } from 'svelte'; + import { Fieldset, Icon, Link, Typography } from '@appwrite.io/pink-svelte'; import { writable } from 'svelte/store'; - $: anyOrgFree = $organizationList.teams?.some( - (org) => (org as Organization)?.billingPlan === BillingPlan.FREE - ); - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i; + export let data; + + let selectedPlan: BillingPlan = data.plan as BillingPlan; + let selectedCoupon: Partial | null = data.coupon; let previousPage: string = base; let showExitModal = false; - - afterNavigate(({ from }) => { - previousPage = from?.url?.pathname || previousPage; - }); - let formComponent: Form; let isSubmitting = writable(false); - let methods: PaymentList; let name: string; - let billingPlan: BillingPlan = BillingPlan.FREE; let paymentMethodId: string; let collaborators: string[] = []; - let couponData: Partial = { - code: null, - status: null, - credits: null - }; let taxId: string; - let billingBudget: number; let showCreditModal = false; - onMount(async () => { - if ($page.url.searchParams.has('coupon')) { - const coupon = $page.url.searchParams.get('coupon'); - try { - const response = await sdk.forConsole.billing.getCoupon(coupon); - couponData = response; - } catch (e) { - couponData = { - code: null, - status: null, - credits: null - }; - } - } - if ($page.url.searchParams.has('name')) { - name = $page.url.searchParams.get('name'); - } - if ($page.url.searchParams.has('plan')) { - const plan = $page.url.searchParams.get('plan'); - if (plan && Object.values(BillingPlan).includes(plan as BillingPlan)) { - billingPlan = plan as BillingPlan; - } - } + afterNavigate(({ from }) => { + previousPage = from?.url?.pathname || previousPage; }); async function loadPaymentMethods() { @@ -92,7 +52,7 @@ try { let org: Organization; - if (billingPlan === BillingPlan.FREE) { + if (selectedPlan === BillingPlan.FREE) { org = await sdk.forConsole.billing.createOrganization( ID.unique(), name, @@ -104,7 +64,7 @@ org = await sdk.forConsole.billing.createOrganization( ID.unique(), name, - billingPlan, + selectedPlan, paymentMethodId, null ); @@ -115,8 +75,8 @@ } //Add coupon - if (couponData?.code) { - await sdk.forConsole.billing.addCredit(org.$id, couponData.code); + if (selectedCoupon?.code) { + await sdk.forConsole.billing.addCredit(org.$id, selectedCoupon.code); trackEvent(Submit.CreditRedeem); } @@ -141,7 +101,7 @@ } trackEvent(Submit.OrganizationCreate, { - plan: tierToPlan(billingPlan)?.name, + plan: tierToPlan(selectedPlan)?.name, budget_cap_enabled: !!billingBudget, members_invited: collaborators?.length }); @@ -162,7 +122,7 @@ } } - $: if (billingPlan !== BillingPlan.FREE) { + $: if (selectedPlan !== BillingPlan.FREE) { loadPaymentMethods(); } @@ -171,57 +131,60 @@ Create organization - Appwrite - - Create organization - -
    - - - - -

    + + +

    + +
    +
    + For more details on our plans, visit our - . -

    - - {#if billingPlan !== BillingPlan.FREE} + pricing page. +
    + +
    + {#if selectedPlan !== BillingPlan.FREE} +
    - - {#if !couponData?.code} - - {/if} +
    + + {#if !selectedCoupon?.code} + {/if} - - - {#if billingPlan !== BillingPlan.FREE} - - {:else} - - {/if} - -
    - - + {/if} + + + {#if selectedPlan !== BillingPlan.FREE} + + {:else} + + {/if} + + - -
    + + - + diff --git a/src/routes/(console)/create-organization/+page.ts b/src/routes/(console)/create-organization/+page.ts new file mode 100644 index 000000000..68b4c3a15 --- /dev/null +++ b/src/routes/(console)/create-organization/+page.ts @@ -0,0 +1,46 @@ +import { BillingPlan } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; +import type { PageLoad } from './$types'; +import type { Coupon } from '$lib/sdk/billing'; +import type { Organization } from '$lib/stores/organization'; + +export const load: PageLoad = async ({ url, parent }) => { + const { organizations } = await parent(); + const coupon = await getCoupon(url); + let plan = getPlanFromUrl(url); + const hasFreeOrganizations = organizations.teams?.some( + (org) => (org as Organization)?.billingPlan === BillingPlan.FREE + ); + if (plan === BillingPlan.FREE && hasFreeOrganizations) { + plan = BillingPlan.PRO; + } + + return { + plan, + coupon, + hasFreeOrganizations, + name: url.searchParams.get('name') ?? '' + }; +}; + +function getPlanFromUrl(url: URL): BillingPlan | null { + if (url.searchParams.has('plan')) { + const plan = url.searchParams.get('plan'); + if (plan && plan in BillingPlan) { + return plan as BillingPlan; + } + } + return BillingPlan.FREE; +} + +async function getCoupon(url: URL): Promise { + if (url.searchParams.has('code')) { + const coupon = url.searchParams.get('code'); + try { + return sdk.forConsole.billing.getCoupon(coupon); + } catch (e) { + return null; + } + } + return null; +} diff --git a/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte b/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte index 0d81d2952..47bc47465 100644 --- a/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte +++ b/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte @@ -65,27 +65,24 @@ rel="noopener noreferrer">view our pricing guide. {:else} - - - - Budget cap limits do not include the base amount of your plan. - - - {#if capActive} - - {/if} - + + + Budget cap limits do not include the base amount of your plan. + + + {#if capActive} + + {/if} {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte index 0510a8b32..bd1a2a419 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte @@ -107,11 +107,7 @@ Details The scheduled payment has failed. - { - retryPayment(invoice); - showFailedError = false; - }} + retryPayment(invoice)} >Try again diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index f17e1b9cc..515483091 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -3,7 +3,6 @@ import { base } from '$app/paths'; import { page } from '$app/stores'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { Alert } from '$lib/components'; import { EstimatedTotalBox, PlanComparisonBox, @@ -12,46 +11,29 @@ import PlanExcess from '$lib/components/billing/planExcess.svelte'; import PlanSelection from '$lib/components/billing/planSelection.svelte'; import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte'; - import Default from '$lib/components/roles/default.svelte'; import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants'; - import { - Button, - Form, - FormList, - InputSelect, - InputTags, - InputTextarea, - Label - } from '$lib/elements/forms'; + import { Button, Form, InputSelect, InputTags, InputTextarea } from '$lib/elements/forms'; import { formatCurrency } from '$lib/helpers/numbers.js'; - import { - WizardSecondaryContainer, - WizardSecondaryContent, - WizardSecondaryFooter - } from '$lib/layout'; + import { Wizard } from '$lib/layout'; import { type Coupon, type PaymentList } from '$lib/sdk/billing'; - import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing'; + import { plansInfo, tierToPlan } from '$lib/stores/billing'; import { addNotification } from '$lib/stores/notifications'; - import { - currentPlan, - organization, - organizationList, - type Organization - } from '$lib/stores/organization'; + import { organization, organizationList, type Organization } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; import { user } from '$lib/stores/user'; import { VARS } from '$lib/system'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; - import { Icon } from '@appwrite.io/pink-svelte'; - import { onMount } from 'svelte'; + import { Alert, Fieldset, Icon, Layout, Link, Typography } from '@appwrite.io/pink-svelte'; import { writable } from 'svelte/store'; export let data; + let selectedPlan: BillingPlan = data.plan as BillingPlan; + let selectedCoupon: Partial | null = data.coupon; + $: anyOrgFree = $organizationList.teams?.find( (org) => (org as Organization)?.billingPlan === BillingPlan.FREE ); - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i; let previousPage: string = base; let showExitModal = false; afterNavigate(({ from }) => { @@ -61,7 +43,6 @@ let formComponent: Form; let isSubmitting = writable(false); let methods: PaymentList; - let billingPlan: Tier = $organization.billingPlan; let paymentMethodId: string; let collaborators: string[] = data?.members?.memberships @@ -69,53 +50,18 @@ 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 selfService: boolean; - - onMount(async () => { - if ($page.url.searchParams.has('code')) { - const coupon = $page.url.searchParams.get('code'); - try { - const response = await sdk.forConsole.billing.getCoupon(coupon); - couponData = response; - } catch (e) { - couponData = { - code: null, - status: null, - credits: null - }; - } - } - if ($page.url.searchParams.has('plan')) { - const plan = $page.url.searchParams.get('plan'); - if (plan && plan in BillingPlan) { - billingPlan = plan as BillingPlan; - } - } - if ($organization?.billingPlan === BillingPlan.SCALE) { - billingPlan = BillingPlan.SCALE; - } else { - billingPlan = BillingPlan.PRO; - } - - selfService = $currentPlan?.selfService ?? true; - }); async function loadPaymentMethods() { methods = await sdk.forConsole.billing.listPaymentMethods(); paymentMethodId = - $organization?.paymentMethodId ?? + data.organization.paymentMethodId ?? methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null; } @@ -131,8 +77,8 @@ async function downgrade() { try { await sdk.forConsole.billing.updatePlan( - $organization.$id, - billingPlan, + data.organization.$id, + selectedPlan, paymentMethodId, null ); @@ -143,14 +89,14 @@ 'Content-Type': 'application/json' }, body: JSON.stringify({ - from: tierToPlan($organization.billingPlan).name, - to: tierToPlan(billingPlan).name, - email: $user.email, + from: tierToPlan(data.organization.billingPlan).name, + to: tierToPlan(selectedPlan).name, + email: data.account.email, reason: feedbackDowngradeOptions.find( (option) => option.value === feedbackDowngradeReason )?.label, - orgId: $organization.$id, - userId: $user.$id, + orgId: data.organization.$id, + userId: data.account.$id, message: feedbackMessage ?? '' }) }); @@ -161,12 +107,12 @@ isHtml: true, message: ` ${$organization.name} will change to ${ - tierToPlan(billingPlan).name + tierToPlan(selectedPlan).name } plan at the end of the current billing cycle.` }); trackEvent(Submit.OrganizationDowngrade, { - plan: tierToPlan(billingPlan)?.name + plan: tierToPlan(selectedPlan)?.name }); } catch (e) { addNotification({ @@ -180,15 +126,15 @@ async function upgrade() { try { const org = await sdk.forConsole.billing.updatePlan( - $organization.$id, - billingPlan, + data.organization.$id, + selectedPlan, paymentMethodId, null ); //Add coupon - if (couponData?.code) { - await sdk.forConsole.billing.addCredit(org.$id, couponData.code); + if (selectedCoupon?.code) { + await sdk.forConsole.billing.addCredit(org.$id, selectedCoupon.code); trackEvent(Submit.CreditRedeem); } @@ -230,7 +176,7 @@ }); trackEvent(Submit.OrganizationUpgrade, { - plan: tierToPlan(billingPlan)?.name + plan: tierToPlan(selectedPlan)?.name }); } catch (e) { addNotification({ @@ -241,124 +187,130 @@ } } - $: isUpgrade = billingPlan > $organization.billingPlan; - $: isDowngrade = billingPlan < $organization.billingPlan; - $: if (billingPlan !== BillingPlan.FREE) { + $: isUpgrade = data.plan > data.organization.billingPlan; + $: isDowngrade = data.plan < data.organization.billingPlan; + $: if (selectedPlan !== BillingPlan.FREE) { loadPaymentMethods(); } - $: isButtonDisabled = $organization.billingPlan === billingPlan; + $: isButtonDisabled = $organization.billingPlan === selectedPlan; Change plan - Appwrite - - Change plan - -
    - -

    - For more details on our plans, visit our - . -

    - {#if !selfService} - Your contract is not eligible for manual changes. Please reach out to schedule - a call or setup a dialog. - {/if} - - - {#if isDowngrade} - {#if billingPlan === BillingPlan.FREE} - - {:else if billingPlan === BillingPlan.PRO && $organization.billingPlan === BillingPlan.SCALE && collaborators?.length > 0} - {@const extraMembers = collaborators?.length ?? 0} - - - Your monthly payments will be adjusted for the Pro plan - - After switching plans, - you will be charged {formatCurrency( - extraMembers * - ($plansInfo?.get(billingPlan)?.addons?.member?.price ?? 0) - )} monthly for {extraMembers} team members. This will be reflected in - your next invoice. - + + + +
    + + For more details on our plans, visit our + pricing page. + + {#if !data.selfService} + + Your contract is not eligible for manual changes. Please reach out to + schedule a call or setup a dialog. + {/if} - {/if} + + + {#if isDowngrade} + {#if selectedPlan === BillingPlan.FREE} + + {:else if selectedPlan === BillingPlan.PRO && data.organization.billingPlan === BillingPlan.SCALE && collaborators?.length > 0} + {@const extraMembers = collaborators?.length ?? 0} + {@const price = formatCurrency( + extraMembers * + ($plansInfo?.get(selectedPlan)?.addons?.member?.price ?? 0) + )} + + + Your monthly payments will be adjusted for the Pro plan + + After switching plans, + you will be charged {price} monthly for {extraMembers} team members. + This will be reflected in your next invoice. + + {/if} + {/if} +
    + - {#if billingPlan !== BillingPlan.FREE && $organization.billingPlan === BillingPlan.FREE} - + {#if selectedPlan !== BillingPlan.FREE && data.organization.billingPlan === BillingPlan.FREE} +
    - - {#if !couponData?.code} - - {/if} - +
    + + {#if !selectedCoupon?.code} + + {/if} {/if} - {#if isDowngrade && billingPlan === BillingPlan.FREE} - - - - + {#if isDowngrade && selectedPlan === BillingPlan.FREE} +
    + + + + +
    {/if} - - - {#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== billingPlan && $organization.billingPlan !== BillingPlan.CUSTOM} - - {:else if $organization.billingPlan !== BillingPlan.CUSTOM} - - {/if} - -
    - - + + + + {#if selectedPlan !== BillingPlan.FREE && data.organization.billingPlan !== selectedPlan && data.organization.billingPlan !== BillingPlan.CUSTOM} + + {:else if data.organization.billingPlan !== BillingPlan.CUSTOM} + + {/if} + + - -
    + + - + diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index e14b3d468..a0a993f11 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -1,11 +1,48 @@ -import { Dependencies } from '$lib/constants'; +import { BillingPlan, Dependencies } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; import type { PageLoad } from './$types'; +import type { Coupon } from '$lib/sdk/billing'; -export const load: PageLoad = async ({ depends, parent }) => { - const { members } = await parent(); - +export const load: PageLoad = async ({ depends, parent, url }) => { + const { members, organization, currentPlan } = await parent(); depends(Dependencies.ORGANIZATION); + + const coupon = await getCoupon(url); + let plan = getPlanFromUrl(url); + + if (organization?.billingPlan === BillingPlan.SCALE) { + plan = BillingPlan.SCALE; + } else { + plan = BillingPlan.PRO; + } + + const selfService = currentPlan?.selfService ?? true; + return { - members + members, + plan, + coupon, + selfService }; }; + +function getPlanFromUrl(url: URL): BillingPlan | null { + if (url.searchParams.has('plan')) { + const plan = url.searchParams.get('plan'); + if (plan && plan in BillingPlan) { + return plan as BillingPlan; + } + } +} + +async function getCoupon(url: URL): Promise { + if (url.searchParams.has('code')) { + const coupon = url.searchParams.get('code'); + try { + return sdk.forConsole.billing.getCoupon(coupon); + } catch (e) { + return null; + } + } + return null; +}