diff --git a/package-lock.json b/package-lock.json index 1f4aae1c1..279f65287 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@popperjs/core": "^2.11.8", "@sentry/svelte": "^7.66.0", "@sentry/tracing": "^7.66.0", - "@stripe/stripe-js": "^2.2.0", + "@stripe/stripe-js": "^3.4.0", "ai": "^2.2.11", "analytics": "^0.8.9", "dayjs": "^1.11.9", @@ -2301,9 +2301,12 @@ } }, "node_modules/@stripe/stripe-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.4.0.tgz", - "integrity": "sha512-WFkQx1mbs2b5+7looI9IV1BLa3bIApuN3ehp9FP58xGg7KL9hCHDECgW3BwO9l9L+xBPVAD7Yjn1EhGe6EDTeA==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-3.4.0.tgz", + "integrity": "sha512-a2kUP7OrsV0SSIk3UxWa+cnrW+PPIyuCbWIBH8vxfHIqmyeQN/d0lsplZJ2h7MlLsU/sB3EyhNBkhLLT+zHwKw==", + "engines": { + "node": ">=12.16" + } }, "node_modules/@sveltejs/adapter-static": { "version": "3.0.1", diff --git a/package.json b/package.json index 7f8ad03b2..f258ae4db 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@popperjs/core": "^2.11.8", "@sentry/svelte": "^7.66.0", "@sentry/tracing": "^7.66.0", - "@stripe/stripe-js": "^2.2.0", + "@stripe/stripe-js": "^3.4.0", "ai": "^2.2.11", "analytics": "^0.8.9", "dayjs": "^1.11.9", diff --git a/src/lib/components/alert.svelte b/src/lib/components/alert.svelte index 20998db9d..f37e83fe1 100644 --- a/src/lib/components/alert.svelte +++ b/src/lib/components/alert.svelte @@ -1,11 +1,13 @@ -{#if $organization?.$id && $organization?.billingPlan === BillingPlan.STARTER && $readOnly && !$page.url.pathname.includes('/console/account')} +{#if $organization?.$id && $organization?.billingPlan === BillingPlan.STARTER && $readOnly && !hideBillingHeaderRoutes.includes($page.url.pathname)} @@ -21,14 +19,14 @@ diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte new file mode 100644 index 000000000..932700919 --- /dev/null +++ b/src/lib/components/billing/estimatedTotalBox.svelte @@ -0,0 +1,113 @@ + + +
+ +

{currentPlan.name} plan

+

{formatCurrency(currentPlan.price)}

+
+ +

Additional seats ({collaborators?.length})

+

+ {formatCurrency(extraSeatsCost)} +

+
+ {#if couponData?.status === 'active'} + +
+

+

+ +
+ {#if couponData.credits > 100} +

+ Credits applied +

+ {:else} + -{formatCurrency(couponData.credits)} + {/if} +
+ {/if} +
+ +

Estimated total

+

+ {formatCurrency(estimatedTotal)} +

+
+ +

+ Your payment method will be charged this amount plus usage fees every 30 days {!currentPlan.trialDays + ? `starting ${toLocaleDate(billingPayDate.toString())}` + : ` after your trial period ends on ${toLocaleDate(trialEndDate.toString())}`}. +

+ + + {#if budgetEnabled} +
+ +
+ {/if} +
+
+
diff --git a/src/lib/components/billing/index.ts b/src/lib/components/billing/index.ts index cfa72bd54..1a5a5680b 100644 --- a/src/lib/components/billing/index.ts +++ b/src/lib/components/billing/index.ts @@ -1,3 +1,7 @@ export { default as PaymentBoxes } from './paymentBoxes.svelte'; export { default as CouponInput } from './couponInput.svelte'; +export { default as SelectPaymentMethod } from './selectPaymentMethod.svelte'; +export { default as UsageRates } from './usageRates.svelte'; +export { default as EstimatedTotalBox } from './estimatedTotalBox.svelte'; +export { default as PlanComparisonBox } from './planComparisonBox.svelte'; export { default as EmptyCardCloud } from './emptyCardCloud.svelte'; diff --git a/src/routes/console/account/payments/paymentModal.svelte b/src/lib/components/billing/paymentModal.svelte similarity index 93% rename from src/routes/console/account/payments/paymentModal.svelte rename to src/lib/components/billing/paymentModal.svelte index 2ac93e0b2..16388b948 100644 --- a/src/routes/console/account/payments/paymentModal.svelte +++ b/src/lib/components/billing/paymentModal.svelte @@ -68,8 +68,14 @@ } - + + + - + diff --git a/src/lib/components/billing/planComparisonBox.svelte b/src/lib/components/billing/planComparisonBox.svelte new file mode 100644 index 000000000..85443bcfb --- /dev/null +++ b/src/lib/components/billing/planComparisonBox.svelte @@ -0,0 +1,50 @@ + + + + + (selectedTab = 'tier-0')}> + Starter + + (selectedTab = 'tier-1')}> + Pro + + + +
+ {#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
  • +
+ {: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
  • +
+ {/if} +
+
diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/planExcess.svelte b/src/lib/components/billing/planExcess.svelte similarity index 100% rename from src/routes/console/wizard/cloudOrganizationChangeTier/planExcess.svelte rename to src/lib/components/billing/planExcess.svelte diff --git a/src/lib/components/billing/selectPaymentMethod.svelte b/src/lib/components/billing/selectPaymentMethod.svelte new file mode 100644 index 000000000..873814887 --- /dev/null +++ b/src/lib/components/billing/selectPaymentMethod.svelte @@ -0,0 +1,124 @@ + + +{#if filteredMethods?.length} + {#if selectedPaymentMethod?.country === 'in'} + + Indian credit or debit card-holders + To comply with RBI regulations in India, Appwrite will ask for verification to charge up + to $150 USD on your payment method. We will never charge more than the cost of your plan + and the resources you use, or your budget cap limit. For higher usage limits, please contact + us. + + {/if} + { + return { + value: method.$id, + label: `${capitalize(method.brand)} ending in ${method.last4}`, + data: [method.brand] + }; + })} + interactiveOutput + let:option={o}> + + + + + + {o.label} + + + + + + + + + + + {o.label} + + + + + + + + +{:else} + +
+

+ + No saved payment methods +

+ +
+
+{/if} + +{#if showPaymentModal && isCloud && hasStripePublicKey} + + + + {#if showTaxId} +
+ +
+ {/if} +
+
+
+{/if} diff --git a/src/routes/console/wizard/cloudOrganization/usageRates.svelte b/src/lib/components/billing/usageRates.svelte similarity index 90% rename from src/routes/console/wizard/cloudOrganization/usageRates.svelte rename to src/lib/components/billing/usageRates.svelte index ac8fb21c5..f5654243a 100644 --- a/src/routes/console/wizard/cloudOrganization/usageRates.svelte +++ b/src/lib/components/billing/usageRates.svelte @@ -10,18 +10,17 @@ TableRow } from '$lib/elements/table'; import { toLocaleDate } from '$lib/helpers/date'; - import { organization } from '$lib/stores/organization'; - import { createOrganization } from './store'; - import { plansInfo, type Tier } from '$lib/stores/billing'; + import { organization, type Organization } from '$lib/stores/organization'; + import { plansInfo } from '$lib/stores/billing'; import { abbreviateNumber, formatCurrency } from '$lib/helpers/numbers'; import { BillingPlan } from '$lib/constants'; export let show = false; - export let tier: Tier; + export let org: Organization; - $: plan = $plansInfo?.get(tier); + $: plan = $plansInfo?.get(org.billingPlan); - $: nextDate = $createOrganization?.name + $: nextDate = org?.name ? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString() : $organization?.billingNextInvoiceDate; @@ -50,7 +49,7 @@ } ]; - $: isFree = tier === BillingPlan.STARTER; + $: isFree = org.billingPlan === BillingPlan.STARTER; @@ -58,12 +57,12 @@ Usage on the Starter plan is limited for the following resources. Next billing period: {toLocaleDate( nextDate )}. - {:else if tier === BillingPlan.PRO} + {:else if org.billingPlan === BillingPlan.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 tier === BillingPlan.SCALE} + {:else if org.billingPlan === BillingPlan.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)}. diff --git a/src/lib/components/billing/validateCreditModal.svelte b/src/lib/components/billing/validateCreditModal.svelte new file mode 100644 index 000000000..961e04144 --- /dev/null +++ b/src/lib/components/billing/validateCreditModal.svelte @@ -0,0 +1,57 @@ + + + + Credits will be applied automatically to your next invoice. + + + + + + + + + + diff --git a/src/lib/components/card.svelte b/src/lib/components/card.svelte index d5a3cbdb7..4cf714226 100644 --- a/src/lib/components/card.svelte +++ b/src/lib/components/card.svelte @@ -5,6 +5,8 @@ isTile?: boolean; isDashed?: boolean; danger?: boolean; + style?: string; + class?: string; }; type ButtonProps = { @@ -26,6 +28,7 @@ export let href: string = null; let classes = ''; export { classes as class }; + export let style: string = ''; function getElement() { switch (true) { @@ -46,6 +49,8 @@ class:is-border-dashed={isDashed} class:is-danger={danger} class:is-allowed-focus={href} + {...$$restProps} + {style} on:click on:keyup={clickOnEnter} role={href || isButton ? 'button' : 'generic'} diff --git a/src/lib/components/cardPlanLimit.svelte b/src/lib/components/cardPlanLimit.svelte index 966337d2f..51fd5242e 100644 --- a/src/lib/components/cardPlanLimit.svelte +++ b/src/lib/components/cardPlanLimit.svelte @@ -1,7 +1,6 @@ @@ -9,8 +8,6 @@

Upgrade your plan to add more {service}

- +
diff --git a/src/lib/components/secondaryTabs.svelte b/src/lib/components/secondaryTabs.svelte index 4cc2a0db6..835f1a1cc 100644 --- a/src/lib/components/secondaryTabs.svelte +++ b/src/lib/components/secondaryTabs.svelte @@ -1,9 +1,10 @@ -
    +
    diff --git a/src/lib/components/support.svelte b/src/lib/components/support.svelte index d6a8f0ef9..ec768ed4a 100644 --- a/src/lib/components/support.svelte +++ b/src/lib/components/support.svelte @@ -7,9 +7,9 @@ import { isCloud } from '$lib/system'; import { organization } from '$lib/stores/organization'; import { BillingPlan } from '$lib/constants'; - import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte'; import { trackEvent } from '$lib/actions/analytics'; import { localeTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date'; + import { upgradeURL } from '$lib/stores/billing'; export let show = false; @@ -33,9 +33,8 @@ {#if $organization?.billingPlan === BillingPlan.STARTER} {/if} diff --git a/src/lib/elements/forms/inputSelectSearch.svelte b/src/lib/elements/forms/inputSelectSearch.svelte index 6d1fad7df..5d6f886ec 100644 --- a/src/lib/elements/forms/inputSelectSearch.svelte +++ b/src/lib/elements/forms/inputSelectSearch.svelte @@ -8,7 +8,7 @@ type Option = $$Generic<{ value: string | boolean | number; label: string; - data?: string[]; + data?: unknown[]; }>; type OptionArray = Option[]; @@ -193,6 +193,17 @@ {/each} + + {#if $$slots.listEnd} +
    +
      +
    • + +
    • +
    +
    + {/if} +
    diff --git a/src/lib/elements/forms/inputTags.svelte b/src/lib/elements/forms/inputTags.svelte index 2d3caac21..c6b088ede 100644 --- a/src/lib/elements/forms/inputTags.svelte +++ b/src/lib/elements/forms/inputTags.svelte @@ -13,6 +13,8 @@ export let readonly = false; export let required = false; export let tooltip: string = null; + export let validityRegex: RegExp = null; + export let validityMessage: string = null; let value = ''; let element: HTMLInputElement; @@ -32,8 +34,13 @@ if (value === '' && ['Enter', 'Tab'].includes(e.key)) { return; } + if (['Enter', 'Tab', ' '].includes(e.key)) { e.preventDefault(); + if (validityRegex && !validityRegex.test(value)) { + error = validityMessage ? validityMessage : 'Invalid value'; + return; + } addValue(); } if (['Backspace', 'Delete'].includes(e.key)) { diff --git a/src/lib/elements/forms/label.svelte b/src/lib/elements/forms/label.svelte index 17c1fb922..0bb328f65 100644 --- a/src/lib/elements/forms/label.svelte +++ b/src/lib/elements/forms/label.svelte @@ -1,4 +1,6 @@ + +
    +
    + + +
    + {#if $$slots.description} +

    + {/if} +
    diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 56980bc30..a298eace4 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -299,7 +299,7 @@ export class Billing { name: string, billingPlan: string, paymentMethodId: string, - billingAddressId: string + billingAddressId: string = undefined ): Promise { const path = `/organizations`; const params = { @@ -356,7 +356,7 @@ export class Billing { organizationId: string, billingPlan: string, paymentMethodId: string, - billingAddressId: string + billingAddressId: string = undefined ): Promise { const path = `/organizations/${organizationId}/plan`; const params = { diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 433706a49..bd6b2881b 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -24,8 +24,6 @@ import { BillingPlan } from '$lib/constants'; import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte'; import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte'; import LimitReached from '$lib/components/billing/alerts/limitReached.svelte'; -import { wizard } from './wizard'; -import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte'; import { trackEvent } from '$lib/actions/analytics'; export type Tier = 'tier-0' | 'tier-1' | 'tier-2'; @@ -114,7 +112,7 @@ export type TierData = { export const tierFree: TierData = { name: 'Starter', - description: 'For personal, passion projects.' + description: 'For personal hobby projects of small scale and students.' }; export const tierPro: TierData = { @@ -239,7 +237,7 @@ export async function checkForUsageLimit(org: Organization) { { name: 'Upgrade plan', method: () => { - wizard.start(ChangeOrganizationTierCloud); + goto(`${base}/console/organization-${org.$id}/change-plan`); trackEvent('click_organization_upgrade', { from: 'button', source: 'limit_reached_notification' @@ -364,3 +362,10 @@ export async function checkForMissingPaymentMethod() { }); } } + +export const upgradeURL = derived( + page, + ($page) => `${base}/console/organization-${$page.data?.organization?.$id}/change-plan` +); + +export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account']; diff --git a/src/routes/console/(migration-wizard)/resource-form.svelte b/src/routes/console/(migration-wizard)/resource-form.svelte index 41d39cee0..66a2fadb6 100644 --- a/src/routes/console/(migration-wizard)/resource-form.svelte +++ b/src/routes/console/(migration-wizard)/resource-form.svelte @@ -193,8 +193,8 @@ isStandalone buttons={[ { - name: 'Learn more', - method() { + slot: 'Learn more', + onClick() { wizard.updateStep((p) => p - 1); } } @@ -213,8 +213,8 @@ isStandalone buttons={[ { - name: 'Edit credentials', - method() { + slot: 'Edit credentials', + onClick() { wizard.updateStep((p) => p - 1); } } diff --git a/src/routes/console/+layout.svelte b/src/routes/console/+layout.svelte index 6c9a44291..dc8ceeb81 100644 --- a/src/routes/console/+layout.svelte +++ b/src/routes/console/+layout.svelte @@ -38,10 +38,9 @@ import { stripe } from '$lib/stores/stripe'; import MobileSupportModal from './wizard/support/mobileSupportModal.svelte'; import { showSupportModal } from './wizard/support/store'; - - import UsageRates from './wizard/cloudOrganization/usageRates.svelte'; import { activeHeaderAlert, consoleVariables } from './store'; import { headerAlert } from '$lib/stores/headerAlert'; + import { UsageRates } from '$lib/components/billing'; function kebabToSentenceCase(str: string) { return str @@ -320,5 +319,5 @@ {/if} {#if isCloud && $showUsageRatesModal} - + {/if} diff --git a/src/routes/console/account/organizations/+page.svelte b/src/routes/console/account/organizations/+page.svelte index 33811a134..08667a5b1 100644 --- a/src/routes/console/account/organizations/+page.svelte +++ b/src/routes/console/account/organizations/+page.svelte @@ -20,9 +20,8 @@ import { daysLeftInTrial, plansInfo } from '$lib/stores/billing'; import { tooltip } from '$lib/actions/tooltip'; import { toLocaleDate } from '$lib/helpers/date'; - import { wizard } from '$lib/stores/wizard'; - import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte'; import { BillingPlan } from '$lib/constants'; + import { goto } from '$app/navigation'; export let data: PageData; let addOrganization = false; @@ -40,7 +39,7 @@ function createOrg() { if (isCloud) { - wizard.start(CreateOrganizationCloud); + goto(`${base}/console/create-organization`); } else addOrganization = true; } diff --git a/src/routes/console/account/payments/paymentMethods.svelte b/src/routes/console/account/payments/paymentMethods.svelte index e8a210ae6..d03911a40 100644 --- a/src/routes/console/account/payments/paymentMethods.svelte +++ b/src/routes/console/account/payments/paymentMethods.svelte @@ -25,7 +25,7 @@ import EditPaymentModal from './editPaymentModal.svelte'; import DeletePaymentModal from './deletePaymentModal.svelte'; import { hasStripePublicKey, isCloud } from '$lib/system'; - import PaymentModal from './paymentModal.svelte'; + import PaymentModal from '$lib/components/billing/paymentModal.svelte'; export let showPayment = false; let showDropdown = []; diff --git a/src/routes/console/changeOrganizationTierCloud.svelte b/src/routes/console/changeOrganizationTierCloud.svelte deleted file mode 100644 index 2e2ad4544..000000000 --- a/src/routes/console/changeOrganizationTierCloud.svelte +++ /dev/null @@ -1,221 +0,0 @@ - - - diff --git a/src/routes/console/create-organization/+page.svelte b/src/routes/console/create-organization/+page.svelte new file mode 100644 index 000000000..58959c166 --- /dev/null +++ b/src/routes/console/create-organization/+page.svelte @@ -0,0 +1,298 @@ + + + + Create organization - Appwrite + + + + Create organization + + + + + + +

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

    +
      +
    • + + +
      +

      + {tierFree.name} +

      +

      {tierFree.description}

      +

      + {formatCurrency(freePlan?.price ?? 0)} +

      +
      +
      +
      +
    • + +
    • + + +
      +

      + {tierPro.name} +

      +

      + {tierPro.description} +

      +

      + {formatCurrency(proPlan?.price ?? 0)} per member/month + usage +

      +
      +
      +
      +
    • +
    + {#if billingPlan === BillingPlan.PRO} + + + + + {#if !couponData?.code} + + {/if} + {/if} + + + {#if billingPlan !== BillingPlan.STARTER} + + {:else} + + {/if} + +
    + + + + + +
    + + diff --git a/src/routes/console/createOrganizationCloud.svelte b/src/routes/console/createOrganizationCloud.svelte deleted file mode 100644 index 459e4b77f..000000000 --- a/src/routes/console/createOrganizationCloud.svelte +++ /dev/null @@ -1,149 +0,0 @@ - - - diff --git a/src/routes/console/onboarding/+page.svelte b/src/routes/console/onboarding/+page.svelte index af2a1e2ba..1f7aac074 100644 --- a/src/routes/console/onboarding/+page.svelte +++ b/src/routes/console/onboarding/+page.svelte @@ -11,14 +11,12 @@ import { Container } from '$lib/layout'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { wizard } from '$lib/stores/wizard'; import { isCloud } from '$lib/system'; import { ID } from '@appwrite.io/console'; import { onMount } from 'svelte'; - import CreateOrganizationCloud from '../createOrganizationCloud.svelte'; import { tierToPlan, type Tier, plansInfo } from '$lib/stores/billing'; - import { createOrganization } from '../wizard/cloudOrganization/store'; import { formatCurrency } from '$lib/helpers/numbers'; + import { base } from '$app/paths'; let name: string; let id: string; @@ -43,7 +41,7 @@ if ($page.url.searchParams.has('type')) { const paramType = $page.url.searchParams.get('type'); if (paramType === 'createPro') { - wizard.start(CreateOrganizationCloud); + goto(`${base}/console/create-organization`); } } } @@ -79,10 +77,7 @@ trackError(error, Submit.OrganizationCreate); } } else { - wizard.start(CreateOrganizationCloud, null, 2); - $createOrganization.name = orgName; - $createOrganization.billingPlan = plan; - $createOrganization.id = id; + goto(`${base}/console/create-organization?name=${orgName}&plan=${plan}`); } } else { try { diff --git a/src/routes/console/organization-[organization]/+page.svelte b/src/routes/console/organization-[organization]/+page.svelte index 7fa8efc52..95dd694b2 100644 --- a/src/routes/console/organization-[organization]/+page.svelte +++ b/src/routes/console/organization-[organization]/+page.svelte @@ -30,7 +30,6 @@ import { readOnly } from '$lib/stores/billing'; import type { RegionList } from '$lib/sdk/billing'; import { onMount } from 'svelte'; - import CreateOrganizationCloud from '../createOrganizationCloud.svelte'; import { organization } from '$lib/stores/organization'; export let data; @@ -125,7 +124,7 @@ if ($page.url.searchParams.has('type')) { const paramType = $page.url.searchParams.get('type'); if (paramType === 'createPro') { - wizard.start(CreateOrganizationCloud); + goto(`${base}/console/create-organization`); } } } diff --git a/src/routes/console/organization-[organization]/billing/+page.svelte b/src/routes/console/organization-[organization]/billing/+page.svelte index 59beb7592..8f7f50b4b 100644 --- a/src/routes/console/organization-[organization]/billing/+page.svelte +++ b/src/routes/console/organization-[organization]/billing/+page.svelte @@ -10,19 +10,18 @@ import PaymentHistory from './paymentHistory.svelte'; import TaxId from './taxId.svelte'; import { Alert, Heading } from '$lib/components'; - import { failedInvoice, paymentMethods } from '$lib/stores/billing'; + import { failedInvoice, paymentMethods, upgradeURL } from '$lib/stores/billing'; import type { PaymentMethodData } from '$lib/sdk/billing'; import { onMount } from 'svelte'; import { page } from '$app/stores'; import { confirmPayment } from '$lib/stores/stripe'; import { sdk } from '$lib/stores/sdk'; import { toLocaleDate } from '$lib/helpers/date'; - import { wizard } from '$lib/stores/wizard'; - import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte'; import { BillingPlan } from '$lib/constants'; import RetryPaymentModal from './retryPaymentModal.svelte'; import { selectedInvoice, showRetryModal } from './store'; import { Button } from '$lib/elements/forms'; + import { goto } from '$app/navigation'; export let data; @@ -37,7 +36,7 @@ onMount(async () => { if ($page.url.searchParams.has('type')) { if ($page.url.searchParams.get('type') === 'upgrade') { - wizard.start(ChangeOrganizationTierCloud); + goto($upgradeURL); } if ( diff --git a/src/routes/console/organization-[organization]/billing/availableCredit.svelte b/src/routes/console/organization-[organization]/billing/availableCredit.svelte index 716605913..a960d897e 100644 --- a/src/routes/console/organization-[organization]/billing/availableCredit.svelte +++ b/src/routes/console/organization-[organization]/billing/availableCredit.svelte @@ -20,8 +20,8 @@ import AddCreditModal from './addCreditModal.svelte'; import { formatCurrency } from '$lib/helpers/numbers'; import { BillingPlan } from '$lib/constants'; - import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte'; import { trackEvent } from '$lib/actions/analytics'; + import { upgradeURL } from '$lib/stores/billing'; let offset = 0; let creditList: CreditList = { @@ -132,8 +132,8 @@ {#if $organization?.billingPlan === BillingPlan.STARTER} . +

    + {#if anyOrgFree && billingPlan === BillingPlan.PRO} + + You are limited to one Starter organization per account. Consider upgrading or + deleting . + + {/if} +
      +
    • + + +
      +

      + {tierFree.name} + {#if $organization.billingPlan === BillingPlan.STARTER} + Current plan + {/if} +

      +

      {tierFree.description}

      +

      + {formatCurrency(freePlan?.price ?? 0)} +

      +
      +
      +
      +
    • + +
    • + + +
      +

      + {tierPro.name} + {#if $organization.billingPlan === BillingPlan.PRO} + Current plan + {/if} +

      +

      + {tierPro.description} +

      +

      + {formatCurrency(proPlan?.price ?? 0)} per member/month + usage +

      +
      +
      +
      +
    • +
    + {#if billingPlan === BillingPlan.PRO && $organization.billingPlan !== BillingPlan.PRO} + + + + + {#if !couponData?.code} + + {/if} + {/if} + {#if !isUpgrade && billingPlan === BillingPlan.STARTER && $organization.billingPlan !== BillingPlan.STARTER} + + + + + {/if} + + + {#if billingPlan !== BillingPlan.STARTER && $organization.billingPlan !== BillingPlan.PRO} + + {:else} + + {/if} + + + + + + + + + + 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 + }; +}; diff --git a/src/routes/console/organization-[organization]/excesLimitModal.svelte b/src/routes/console/organization-[organization]/excesLimitModal.svelte index 848f1b51e..a769a87f0 100644 --- a/src/routes/console/organization-[organization]/excesLimitModal.svelte +++ b/src/routes/console/organization-[organization]/excesLimitModal.svelte @@ -1,19 +1,17 @@ - - - Billing address - Add a billing address for your organization. - - - {#if addressList} - - -
    -

    {element.streetAddress}

    - {#if element?.addressLine2} -

    {element.addressLine2}

    - {/if} -

    {element.city}

    -

    {element.state}

    -

    {element.postalCode}

    -

    {element.country}

    -
    -
    - - - Add new billing address - - - - - - - - - - - - - -
    - {/if} -
    -
    diff --git a/src/routes/console/wizard/cloudOrganization/confirmDetails.svelte b/src/routes/console/wizard/cloudOrganization/confirmDetails.svelte deleted file mode 100644 index e3eb21d55..000000000 --- a/src/routes/console/wizard/cloudOrganization/confirmDetails.svelte +++ /dev/null @@ -1,138 +0,0 @@ - - - - Details - - Confirm the details of your new organization{!plan.trialDays - ? '.' - : ' and start your free trial.'} - - -

    Organization name

    -
    -

    {$createOrganization.name}

    - {#if $createOrganization?.id} - {$createOrganization.id} - {/if} -
    - - {#if $createOrganization.billingPlan !== BillingPlan.STARTER} -
    -

    Additional members

    -

    {collaboratorsNumber} members

    -
    -
    -

    Payment

    - {#await fetchCard()} -
    -
    -
    - {:then card} - -

    - {card?.brand} ending in {card?.last4} -

    - -
    - {/await} -
    - {/if} - - {#if $createOrganization.billingPlan !== BillingPlan.STARTER} - ($createOrganization.couponCode = e.detail.code)} /> - -

    {plan.name} plan

    -

    {formatCurrency(plan.price)}

    -
    - -

    Additional members ({collaboratorsNumber})

    -

    {formatCurrency(collaboratorPrice * collaboratorsNumber)}

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

    Credits applied ({couponData.credits})

    -

    -{formatCurrency(couponData.credits)}

    -
    - {/if} -
    - {/if} - {@const estimatedTotal = - couponData?.status === 'active' - ? totalExpences - couponData.credits >= 0 - ? totalExpences - couponData.credits - : 0 - : totalExpences} - -

    Estimated total

    -

    - {formatCurrency(estimatedTotal)} -

    -
    - - {#if $createOrganization.billingPlan !== BillingPlan.STARTER} -

    - This amount, and any additional usage fees, will be charged on a recurring 30-day - billing cycle{!plan.trialDays - ? '' - : ` after your trial period ends on ${toLocaleDate(billingPayDate.toString())}`}. -

    - {/if} - - diff --git a/src/routes/console/wizard/cloudOrganization/inviteMembers.svelte b/src/routes/console/wizard/cloudOrganization/inviteMembers.svelte deleted file mode 100644 index 1bfe98fc6..000000000 --- a/src/routes/console/wizard/cloudOrganization/inviteMembers.svelte +++ /dev/null @@ -1,103 +0,0 @@ - - - - Invites - - Invite team members to collaborate with you in the Appwrite console. Members will have - access to all services and payment data within your organization. - - - - {#if $createOrganization.billingPlan === BillingPlan.SCALE} - You can add unlimited organization members on the {plan.name} plan at no cost. Each member - added will receive an email invite to your organization on completion. - {:else if $createOrganization.billingPlan === BillingPlan.PRO} - You can add unlimited organization members on the {plan.name} plan for - {formatCurrency(plan.addons.member.price)} each per month. Each member added will - receive an email invite to your organization on completion. - {/if} - - -
    -
    - - - - - -
    -
    - - {#if $createOrganization?.collaborators?.length} -
    - - - Collaborator - {#if $createOrganization.billingPlan === BillingPlan.PRO} - Cost - {/if} - - - - {#each $createOrganization.collaborators as collaborator} - - {collaborator} - {#if $createOrganization.billingPlan === BillingPlan.PRO} - {formatCurrency(15)} - {/if} - - - - - {/each} - -
    -
    - {/if} -
    diff --git a/src/routes/console/wizard/cloudOrganization/organizationDetails.svelte b/src/routes/console/wizard/cloudOrganization/organizationDetails.svelte deleted file mode 100644 index 67bed9a75..000000000 --- a/src/routes/console/wizard/cloudOrganization/organizationDetails.svelte +++ /dev/null @@ -1,142 +0,0 @@ - - - - Organization details - - - - {#if !showCustomId} -
    - (showCustomId = !showCustomId)}> - -
    - {:else} - - {/if} -
    - -

    Plan

    -

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

    -
      -
    • - - -
      -

      - {tierFree.name} - {formatCurrency(freePlan?.price ?? 0)}/month -

      -

      {tierFree.description}

      -
      -
      -
      -
    • - -
    • - - -
      -

      - {tierPro.name} - {formatCurrency(proPlan?.price ?? 0)}/month per - organization member + exta usage -

      -

      - {tierPro.description} -

      -
      - {#if proPlan?.trialDays} - 14 DAY FREE TRIAL - {/if} -
      -
      -
    • -
    • - - -
      -

      - {tierScale.name} - {formatCurrency(scalePlan?.price ?? 0)}/month + extra - usage -

      -

      - {tierScale.description} -

      -
      -
      - COMING SOON -
      -
      -
      -
    • -
    -
    diff --git a/src/routes/console/wizard/cloudOrganization/paymentDetails.svelte b/src/routes/console/wizard/cloudOrganization/paymentDetails.svelte deleted file mode 100644 index 956064b66..000000000 --- a/src/routes/console/wizard/cloudOrganization/paymentDetails.svelte +++ /dev/null @@ -1,117 +0,0 @@ - - - - Payment details - - Add a payment method to your organization. {#if $plansInfo.get($createOrganization.billingPlan)?.trialDays}You - will not be charged until your trial ends on {toLocaleDate(billingPayDate.toString())}. - {/if} - - - - - - -

    - Restrict your resource usage by setting a budget cap. . -

    - {#if budgetEnabled} -
    - -
    - {/if} -
    -
    -
    diff --git a/src/routes/console/wizard/cloudOrganization/store.ts b/src/routes/console/wizard/cloudOrganization/store.ts deleted file mode 100644 index 55a53175c..000000000 --- a/src/routes/console/wizard/cloudOrganization/store.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BillingPlan } from '$lib/constants'; -import type { WizardStepsType } from '$lib/layout/wizard.svelte'; -import { type Tier } from '$lib/stores/billing'; -import { writable } from 'svelte/store'; - -export const createOrgSteps = writable(new Map()); -export const createOrganizationFinalAction = writable('Create'); - -export const createOrganization = writable<{ - id?: string; - name: string; - billingPlan: Tier; - paymentMethodId: string; - billingAddressId: string; - collaborators?: string[]; - billingBudget?: number; - taxId?: string; - couponCode?: string; -}>({ - id: null, - name: null, - billingPlan: BillingPlan.PRO, - paymentMethodId: null, - collaborators: [], - billingAddressId: null, - taxId: null, - couponCode: null -}); diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/addressDetails.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/addressDetails.svelte deleted file mode 100644 index 88b2214aa..000000000 --- a/src/routes/console/wizard/cloudOrganizationChangeTier/addressDetails.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - - - Billing address - Add a billing address for your organization. - - - {#if addressList} - - -
    -

    {element.streetAddress}

    - {#if element?.addressLine2} -

    {element.addressLine2}

    - {/if} -

    {element.city}

    -

    {element.state}

    -

    {element.postalCode}

    -

    {element.country}

    -
    -
    - - - Add new billing address - - - - - - - - - - - - - -
    - {/if} -
    -
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/choosePlan.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/choosePlan.svelte deleted file mode 100644 index 77430ca0d..000000000 --- a/src/routes/console/wizard/cloudOrganizationChangeTier/choosePlan.svelte +++ /dev/null @@ -1,195 +0,0 @@ - - - - Plan selection - -

    Plan

    -

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

    -
      -
    • - - -
      -

      - {tierFree.name} - {formatCurrency(freePlan.price)}/month -

      -

      {tierFree.description}

      -
      -
      - {#if $organization.billingPlan === BillingPlan.STARTER} - CURRENT PLAN - {/if} -
      -
      -
      -
    • -
    • - - -
      -

      - {tierPro.name} - {formatCurrency(proPlan.price)}/month per organization - member + extra usage -

      -

      - {tierPro.description} -

      -
      -
      - {#if $organization.billingPlan === BillingPlan.PRO} - CURRENT PLAN - {:else if proPlan?.trialDays} - {proPlan?.trialDays} DAY FREE TRIAL - {/if} -
      -
      -
      -
    • -
    • - - -
      -

      - {tierScale.name} - {formatCurrency(scalePlan.price)}/month + extra usage -

      -

      - {tierScale.description} -

      -
      -
      - COMING SOON -
      -
      -
      -
    • -
    -
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/confirmDetails.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/confirmDetails.svelte deleted file mode 100644 index 51ee8aeed..000000000 --- a/src/routes/console/wizard/cloudOrganizationChangeTier/confirmDetails.svelte +++ /dev/null @@ -1,161 +0,0 @@ - - -{#if downgradeToStarter} - - Change confirmation - - Your feedback is important to us and helps us improve the services Appwrite offers. - Please let us know if there is a specific reason for changing your plan. - - - - - - -{:else} - - Details - - Confirm the details of your new organization{!$plansInfo.get( - $changeOrganizationTier.billingPlan - )?.trialDays - ? '.' - : ' and start your free trial.'} - - -

    Organization name

    -
    -

    {$organization.name}

    -
    - - {#if $changeOrganizationTier.billingPlan !== BillingPlan.STARTER} -
    -

    Additional members

    -

    {collaboratorsNumber} members

    -
    -
    -

    Payment

    - {#await fetchCard()} -
    -
    -
    - {:then card} - -

    - Card ending in {card.last4} -

    - -
    - {/await} -
    - {/if} - - - {#if $changeOrganizationTier.billingPlan !== BillingPlan.STARTER} - ($changeOrganizationTier.couponCode = e.detail.code)} /> - -

    {plan.name} plan

    -

    {formatCurrency(plan.price)}

    -
    - -

    Additional members ({collaboratorsNumber})

    -

    {formatCurrency(collaboratorPrice * collaboratorsNumber)}

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

    Credits applied ({couponData.credits})

    -

    -{formatCurrency(couponData.credits)}

    -
    - {/if} - {/if} - {@const estimatedTotal = - couponData?.status === 'active' - ? totalExpences - couponData.credits >= 0 - ? totalExpences - couponData.credits - : 0 - : totalExpences} -
    - -

    Estimated total

    -

    - {formatCurrency(estimatedTotal)} -

    -
    - -

    - This amount, and any additional usage fees, will be charged on a recurring 30-day - billing cycle{!$plansInfo.get($changeOrganizationTier.billingPlan)?.trialDays - ? '' - : ` after your trial period ends on ${toLocaleDate(billingPayDate.toString())}`}. -

    - - -{/if} diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/inviteMembers.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/inviteMembers.svelte deleted file mode 100644 index 0695e7e0f..000000000 --- a/src/routes/console/wizard/cloudOrganizationChangeTier/inviteMembers.svelte +++ /dev/null @@ -1,119 +0,0 @@ - - - - Invites - - Invite team members to collaborate with you in the Appwrite console. Members will have - access to all services and payment data within your organization. - - - - {#if $changeOrganizationTier.billingPlan === BillingPlan.SCALE} - You can add unlimited organization members on the {plan.name} plan at no cost. Each member - added will receive an email invite to your organization on completion. - {:else if $changeOrganizationTier.billingPlan === BillingPlan.PRO} - You can add unlimited organization members on the {plan.name} plan for - {formatCurrency(plan.addons.member.price)} each per month. Each member added will - receive an email invite to your organization on completion. - {/if} - - -
    -
    - - - - - -
    -
    - - {#if $changeOrganizationTier?.collaborators?.length} -
    - - - Collaborator - {#if $changeOrganizationTier.billingPlan === BillingPlan.PRO} - Cost - {/if} - - - - {#each $changeOrganizationTier.collaborators as collaborator} - - {collaborator} - {#if $changeOrganizationTier.billingPlan === BillingPlan.PRO} - {formatCurrency(15)} - {/if} - - - - - {/each} - -
    -
    - {/if} -
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/paymentDetails.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/paymentDetails.svelte deleted file mode 100644 index 99116f394..000000000 --- a/src/routes/console/wizard/cloudOrganizationChangeTier/paymentDetails.svelte +++ /dev/null @@ -1,119 +0,0 @@ - - - - Payment details - - Confirm the payment method for your organization. - - - - - - -

    - Restrict your resource usage by setting a budget cap. . -

    - {#if budgetEnabled} -
    - -
    - {/if} -
    -
    -
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/store.ts b/src/routes/console/wizard/cloudOrganizationChangeTier/store.ts deleted file mode 100644 index 57463d106..000000000 --- a/src/routes/console/wizard/cloudOrganizationChangeTier/store.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { BillingPlan } from '$lib/constants'; -import type { WizardStepsType } from '$lib/layout/wizard.svelte'; -import { type Tier } from '$lib/stores/billing'; -import { writable } from 'svelte/store'; - -export const changeTierSteps = writable(new Map()); -export const isUpgrade = writable(false); -export const changeOrganizationFinalAction = writable('Create'); - -export const changeOrganizationTier = writable<{ - billingPlan: Tier; - paymentMethodId: string; - billingAddressId: string; - billingBudget?: number; - collaborators?: string[]; - isOverLimit?: boolean; - limitOverflow?: { - bandwidth?: number; - executions?: number; - storage?: number; - users?: number; - members?: number; - }; - taxId?: string; - feedbackMessage?: string; - feedbackDowngradeReason?: string; - couponCode?: string; -}>({ - billingPlan: BillingPlan.PRO, - paymentMethodId: null, - collaborators: [], - isOverLimit: false, - billingAddressId: null, - taxId: null -}); - -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/routes/console/wizard/cloudOrganizationChangeTier/usageExcess.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/usageExcess.svelte deleted file mode 100644 index e5bd500fb..000000000 --- a/src/routes/console/wizard/cloudOrganizationChangeTier/usageExcess.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Your usage exceeds the limits of the {tierToPlan($organization.billingPlan).name} plan - - View the current usage for {$organization.name} and where you will exceed the limits of - your new plan. - - - - diff --git a/tests/e2e/journeys/upgrade-free-tier.spec.ts b/tests/e2e/journeys/upgrade-free-tier.spec.ts index 5c60a3872..63b58b53d 100644 --- a/tests/e2e/journeys/upgrade-free-tier.spec.ts +++ b/tests/e2e/journeys/upgrade-free-tier.spec.ts @@ -1,20 +1,19 @@ import { test } from '@playwright/test'; import { registerUserStep } from '../steps/account'; import { createFreeProject } from '../steps/free-project'; -import { enterAddress, enterCreditCard } from '../steps/pro-project'; +import { enterCreditCard } from '../steps/pro-project'; test('upgrade - free tier', async ({ page }) => { await registerUserStep(page); await createFreeProject(page); await test.step('upgrade project', async () => { - await page.getByRole('button', { name: 'upgrade' }).click(); + await page.getByRole('link', { name: 'upgrade' }).click(); + await page.waitForURL('/console/organization-**/change-plan'); await page.locator('input[value="tier-1"]').click(); - await page.getByRole('button', { name: 'next' }).click(); + await page.getByRole('button', { name: ' Add', exact: true }).click(); await enterCreditCard(page); - await enterAddress(page); // skip members - await page.getByRole('button', { name: 'next' }).click(); - await page.getByRole('button', { name: 'create' }).click(); + await page.getByRole('button', { name: 'change plan' }).click(); await page.waitForURL('/console/organization-**'); }); }); diff --git a/tests/e2e/steps/pro-project.ts b/tests/e2e/steps/pro-project.ts index 571e85ca0..287510713 100644 --- a/tests/e2e/steps/pro-project.ts +++ b/tests/e2e/steps/pro-project.ts @@ -13,15 +13,7 @@ export async function enterCreditCard(page: Page) { await stripe.locator('id=Field-expiryInput').fill('1250'); await stripe.locator('id=Field-cvcInput').fill('123'); await stripe.locator('id=Field-countryInput').selectOption('DE'); - await page.getByRole('button', { name: 'Next' }).click(); -} - -export async function enterAddress(page: Page) { - await page.locator('id=country').selectOption('US'); - await page.locator('id=address').fill('123 Test St'); - await page.locator('id=city').fill('Test City'); - await page.locator('id=state').fill('Test State'); - await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Add', exact: true }).click(); } export async function createProProject(page: Page): Promise { @@ -31,12 +23,11 @@ export async function createProProject(page: Page): Promise { await page.locator('id=name').fill('test org'); await page.locator('id=plan').selectOption('tier-1'); await page.getByRole('button', { name: 'get started' }).click(); + await page.waitForURL('/console/create-organization**'); + await page.getByRole('button', { name: ' Add', exact: true }).click(); await enterCreditCard(page); - await enterAddress(page); // skip members - await page.getByRole('button', { name: 'next' }).click(); - // start pro trial - await page.getByRole('button', { name: 'create' }).click(); + await page.getByRole('button', { name: 'create organization' }).click(); await page.waitForURL('/console/organization-**'); return getOrganizationIdFromUrl(page.url());