mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #1085 from appwrite/refactor-org-plan-change
refactor: org plan change flow
This commit is contained in:
Generated
+9
-9
@@ -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",
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
: grossCost;
|
||||
</script>
|
||||
|
||||
<Box class="u-margin-block-start-32 u-flex u-flex-vertical u-gap-16" radius="small">
|
||||
<Box class="u-margin-block-start-32 u-flex u-flex-vertical u-gap-8" radius="small">
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">{currentPlan.name} plan</p>
|
||||
<p class="text">{formatCurrency(currentPlan.price)}</p>
|
||||
|
||||
@@ -68,7 +68,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FakeModal bind:show title="Add payment method" bind:error onSubmit={handleSubmit}>
|
||||
<FakeModal
|
||||
bind:show
|
||||
title="Add payment method"
|
||||
bind:error
|
||||
onSubmit={handleSubmit}
|
||||
headerDivider={false}>
|
||||
<FormList gap={16}>
|
||||
<slot />
|
||||
<InputText
|
||||
@@ -90,7 +95,7 @@
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit disabled={!name}>Save</Button>
|
||||
<Button submit disabled={!name}>Add</Button>
|
||||
</svelte:fragment>
|
||||
</FakeModal>
|
||||
|
||||
|
||||
@@ -25,25 +25,25 @@
|
||||
<div class="u-margin-block-start-24">
|
||||
{#if selectedTab === 'tier-0'}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="list u-margin-block-start-8">
|
||||
<li class="list-item">
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
<li>
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions} Functions
|
||||
per project
|
||||
</li>
|
||||
<li class="list-item">Limited to 1 organization member</li>
|
||||
<li class="list-item">{plan.bandwidth}GB bandwidth</li>
|
||||
<li class="list-item">{plan.storage}GB storage</li>
|
||||
<li class="list-item">{formatNum(plan.executions)} executions</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
</ul>
|
||||
{:else if selectedTab === 'tier-1'}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="list u-margin-block-start-8">
|
||||
<li class="list-item">Everything in the Starter plan, plus:</li>
|
||||
<li class="list-item">Unlimited databases, buckets, functions</li>
|
||||
<li class="list-item">{plan.bandwidth}GB bandwidth</li>
|
||||
<li class="list-item">{plan.storage}GB storage</li>
|
||||
<li class="list-item">{formatNum(plan.executions)} executions</li>
|
||||
<li class="list-item">Email support</li>
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
<li>Everything in the Starter plan, plus:</li>
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
import { Card, CreditCardBrandImage } from '..';
|
||||
import { Alert, Card, CreditCardBrandImage } from '..';
|
||||
import PaymentModal from './paymentModal.svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
|
||||
@@ -27,9 +27,20 @@
|
||||
});
|
||||
|
||||
$: filteredMethods = methods?.paymentMethods?.filter((method) => !!method?.last4);
|
||||
|
||||
$: selectedPaymentMethod = methods?.paymentMethods?.find((method) => method.$id === value);
|
||||
</script>
|
||||
|
||||
{#if filteredMethods?.length}
|
||||
{#if selectedPaymentMethod?.country === 'in'}
|
||||
<Alert type="warning">
|
||||
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
|
||||
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.
|
||||
</Alert>
|
||||
{/if}
|
||||
<InputSelectSearch
|
||||
id="method"
|
||||
label="Payment method"
|
||||
@@ -73,7 +84,7 @@
|
||||
</svelte:fragment>
|
||||
</InputSelectSearch>
|
||||
{:else}
|
||||
<Card isDashed style="--p-card-padding: 1rem; --p-card-bg-color: transparent" isTile>
|
||||
<Card isDashed style="--p-card-padding: 0.75rem; --p-card-bg-color: transparent" isTile>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p>
|
||||
<span class="icon-exclamation-circle"></span>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</script>
|
||||
|
||||
<header class="wizard-secondary-header">
|
||||
<div class="u-flex u-main-space-between u-gap-32">
|
||||
<Heading size={1} tag="h1"><slot /></Heading>
|
||||
<div class="u-flex u-main-space-between u-gap-32 u-cross-center">
|
||||
<Heading size={5} tag="h1"><slot /></Heading>
|
||||
<Button text round class="u-margin-block-start-8" ariaLabel="close modal" {href}>
|
||||
<span class="icon-x u-font-size-20" aria-hidden="true"></span>
|
||||
</Button>
|
||||
|
||||
@@ -356,7 +356,7 @@ export class Billing {
|
||||
organizationId: string,
|
||||
billingPlan: string,
|
||||
paymentMethodId: string,
|
||||
billingAddressId: string
|
||||
billingAddressId: string = undefined
|
||||
): Promise<Organization> {
|
||||
const path = `/organizations/${organizationId}/plan`;
|
||||
const params = {
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
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, feedbackDowngradeOptions } from '$lib/constants';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormList,
|
||||
InputSelect,
|
||||
InputTags,
|
||||
InputTextarea,
|
||||
Label
|
||||
} from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
WizardSecondaryFooter,
|
||||
WizardSecondaryHeader
|
||||
} from '$lib/layout';
|
||||
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 { 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
|
||||
);
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
|
||||
let previousPage: string = `${base}/console`;
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
previousPage = from?.url?.pathname || previousPage;
|
||||
});
|
||||
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
let methods: PaymentList;
|
||||
let billingPlan: Tier = $organization.billingPlan;
|
||||
let paymentMethodId: string;
|
||||
let collaborators: string[] =
|
||||
data?.members?.memberships
|
||||
?.map((m) => {
|
||||
if (m.userEmail !== $user.email) return m.userEmail;
|
||||
})
|
||||
?.filter(Boolean) ?? [];
|
||||
let couponData: Partial<Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
let taxId: string;
|
||||
let billingBudget: number;
|
||||
let showCreditModal = false;
|
||||
|
||||
let feedbackDowngradeReason: string;
|
||||
let feedbackMessage: string;
|
||||
|
||||
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('plan')) {
|
||||
const plan = $page.url.searchParams.get('plan');
|
||||
if (plan && plan in BillingPlan) {
|
||||
billingPlan = plan as BillingPlan;
|
||||
}
|
||||
}
|
||||
billingPlan = $organization.billingPlan;
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
methods = await sdk.forConsole.billing.listPaymentMethods();
|
||||
|
||||
paymentMethodId =
|
||||
$organization?.paymentMethodId ??
|
||||
methods.paymentMethods.find((method) => !!method?.last4)?.$id ??
|
||||
null;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (billingPlan === BillingPlan.STARTER) {
|
||||
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: `
|
||||
<b>${$organization.name}</b> 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)
|
||||
);
|
||||
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: `
|
||||
<b>${$organization.name}</b> 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);
|
||||
}
|
||||
}
|
||||
|
||||
$: 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;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Change plan - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<WizardSecondaryContainer>
|
||||
<WizardSecondaryHeader href={previousPage}>Change plan</WizardSecondaryHeader>
|
||||
<WizardSecondaryContent>
|
||||
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
|
||||
<Label class="u-margin-block-start-16">Select plan</Label>
|
||||
<p class="text">
|
||||
For more details on our plans, visit our
|
||||
<Button href="https://appwrite.io/pricing" external link>pricing page</Button>.
|
||||
</p>
|
||||
{#if anyOrgFree && billingPlan === BillingPlan.PRO}
|
||||
<Alert type="warning">
|
||||
You are limited to one Starter organization per account. Consider upgrading or
|
||||
deleting <Button link href={`${base}/console/organization-${anyOrgFree.$id}`}
|
||||
>{anyOrgFree.name}</Button
|
||||
>.
|
||||
</Alert>
|
||||
{/if}
|
||||
<ul
|
||||
class="u-flex u-gap-16 u-margin-block-start-8"
|
||||
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
value="tier-0"
|
||||
disabled={!!anyOrgFree}
|
||||
tooltipShow={!!anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account.">
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
{#if $organization.billingPlan === BillingPlan.STARTER}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">{tierFree.description}</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard name="plan" bind:group={billingPlan} value="tier-1">
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
{#if $organization.billingPlan === BillingPlan.PRO}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
{#if billingPlan === BillingPlan.PRO && $organization.billingPlan !== BillingPlan.PRO}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
tooltip="Invited members will have access to all services and payment data within your organization"
|
||||
placeholder="Enter email address(es)"
|
||||
validityRegex={emailRegex}
|
||||
validityMessage="Invalid email address"
|
||||
id="members" />
|
||||
<SelectPaymentMethod bind:methods bind:value={paymentMethodId} bind:taxId />
|
||||
</FormList>
|
||||
{#if !couponData?.code}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
class="u-margin-block-start-16"
|
||||
on:click={() => (showCreditModal = true)}>
|
||||
<span class="icon-plus"></span> <span class="text">Add credits</span>
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !isUpgrade && billingPlan === BillingPlan.STARTER && $organization.billingPlan !== BillingPlan.STARTER}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<InputSelect
|
||||
id="reason"
|
||||
label="What made you decide to change your plan?"
|
||||
placeholder="Select one"
|
||||
required
|
||||
options={feedbackDowngradeOptions}
|
||||
bind:value={feedbackDowngradeReason} />
|
||||
<InputTextarea
|
||||
id="comment"
|
||||
label="Your feedback here"
|
||||
placeholder="Enter feedback"
|
||||
bind:value={feedbackMessage} />
|
||||
</FormList>
|
||||
{/if}
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if billingPlan !== BillingPlan.STARTER && $organization.billingPlan !== BillingPlan.PRO}
|
||||
<EstimatedTotalBox
|
||||
{billingPlan}
|
||||
{collaborators}
|
||||
bind:couponData
|
||||
bind:billingBudget />
|
||||
{:else}
|
||||
<PlanComparisonBox />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</WizardSecondaryContent>
|
||||
|
||||
<WizardSecondaryFooter>
|
||||
<Button fullWidthMobile href={`${base}/console`} secondary>Cancel</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting || isButtonDisabled}>
|
||||
Change plan
|
||||
</Button>
|
||||
</WizardSecondaryFooter>
|
||||
</WizardSecondaryContainer>
|
||||
|
||||
<ValidateCreditModal bind:show={showCreditModal} bind:couponData />
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export const load = async ({ depends, parent }) => {
|
||||
const { members } = await parent();
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
|
||||
return {
|
||||
members
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user