mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'billing' of https://github.com/appwrite/console into fix-billing-usage
This commit is contained in:
@@ -16,7 +16,6 @@
|
||||
return;
|
||||
};
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
|
||||
let backdrop: HTMLDivElement;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let external = false;
|
||||
export let href: string = null;
|
||||
export let fullWidth = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let ariaLabel: string = null;
|
||||
export let noMargin = false;
|
||||
export let event: string = null;
|
||||
@@ -47,6 +48,7 @@
|
||||
text && 'is-text',
|
||||
danger && 'is-danger',
|
||||
fullWidth && 'is-full-width',
|
||||
fullWidthMobile && 'is-full-width-mobile',
|
||||
noMargin && 'u-padding-inline-0',
|
||||
classes
|
||||
]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let title: string;
|
||||
</script>
|
||||
|
||||
<section class="alert is-action is-action-and-top-sticky is-danger u-sep-block-end">
|
||||
<div class="alert-grid">
|
||||
<span class="icon-info" aria-hidden="true" />
|
||||
<div class="alert-content">
|
||||
{#if title || $$slots.title}
|
||||
<h6 class="alert-title">
|
||||
<slot name="title">
|
||||
{title}
|
||||
</slot>
|
||||
</h6>
|
||||
{/if}
|
||||
<p class="alert-message">
|
||||
<slot />
|
||||
</p>
|
||||
</div>
|
||||
{#if $$slots.buttons}
|
||||
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -17,3 +17,4 @@ export { default as Activity } from './activity.svelte';
|
||||
export { default as Progress } from './progress.svelte';
|
||||
export { default as GridHeader } from './gridHeader.svelte';
|
||||
export { default as ContainerHeader } from './containerHeader.svelte';
|
||||
export { default as HeaderAlert } from './headerAlert.svelte';
|
||||
|
||||
+23
-2
@@ -37,6 +37,7 @@ export type Invoice = {
|
||||
to: string;
|
||||
status: string;
|
||||
dueAt: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
|
||||
export type InvoiceList = {
|
||||
@@ -44,6 +45,15 @@ export type InvoiceList = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type Coupon = {
|
||||
$id: string;
|
||||
code: string;
|
||||
credits: number;
|
||||
expiration: string;
|
||||
status: string; // 'active' | 'disabled' | 'expired'
|
||||
validity: number;
|
||||
};
|
||||
|
||||
export type Credit = {
|
||||
/**
|
||||
* Credit ID.
|
||||
@@ -159,8 +169,19 @@ export type AggregationList = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type AllowedRegions =
|
||||
| 'eu-de'
|
||||
| 'us-nyc'
|
||||
| 'us-sfo'
|
||||
| 'ap-in'
|
||||
| 'eu-gb'
|
||||
| 'eu-nl'
|
||||
| 'ap-sg'
|
||||
| 'ap-ca'
|
||||
| 'ap-au';
|
||||
|
||||
export type Region = {
|
||||
$id: string;
|
||||
$id: AllowedRegions;
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
default: boolean;
|
||||
@@ -571,7 +592,7 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async getCoupon(couponId: string): Promise<Partial<Credit>> {
|
||||
async getCoupon(couponId: string): Promise<Coupon> {
|
||||
const path = `/console/coupons/${couponId}`;
|
||||
const params = {
|
||||
couponId
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { INTERVAL } from '$lib/constants';
|
||||
import { Logs } from '$lib/layout';
|
||||
import { HeaderAlert, Logs } from '$lib/layout';
|
||||
import Footer from '$lib/layout/footer.svelte';
|
||||
import Header from '$lib/layout/header.svelte';
|
||||
import SideNavigation from '$lib/layout/navigation.svelte';
|
||||
@@ -44,6 +44,7 @@
|
||||
import ExcesLimitModal from './organization-[organization]/excesLimitModal.svelte';
|
||||
import { showExcess } from './organization-[organization]/store';
|
||||
import UsageRates from './wizard/cloudOrganization/usageRates.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
function kebabToSentenceCase(str: string) {
|
||||
return str
|
||||
@@ -327,6 +328,7 @@
|
||||
const payment = await sdk.forConsole.billing.getPaymentMethod(
|
||||
$organization.paymentMethodId
|
||||
);
|
||||
if (!payment?.expiryYear) return;
|
||||
const year = new Date().getFullYear();
|
||||
const month = new Date().getMonth();
|
||||
const expiredMessage = `The default payment method for <b>${$organization.name}</b> has expired`;
|
||||
@@ -393,33 +395,35 @@
|
||||
<svelte:fragment slot="alert">
|
||||
{#if $failedInvoice && !$page.url.pathname.includes('/console/account')}
|
||||
{@const daysPassed = diffDays(new Date($failedInvoice.dueAt), new Date())}
|
||||
<section class="alert is-action is-action-and-top-sticky is-danger u-sep-block-end">
|
||||
<div class="alert-grid">
|
||||
<span class="icon-info" aria-hidden="true" />
|
||||
<div class="alert-content">
|
||||
<h6 class="alert-title">Your projects are at risk</h6>
|
||||
<p class="alert-message">
|
||||
{#if daysPassed > 30}
|
||||
Your scheduled payment on <b
|
||||
>{toLocaleDate($failedInvoice?.dueAt)}</b> failed. To resume write
|
||||
access of your organization, please update your billing details.
|
||||
{:else}
|
||||
Your scheduled payment on <b
|
||||
>{toLocaleDate($failedInvoice?.dueAt)}</b> failed. Access to paid
|
||||
projects within this organization will be disabled if no action is taken
|
||||
within 30 days.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
|
||||
<a
|
||||
href={`${base}/console/organization-${$failedInvoice?.teamId}/billing#paymentMethods`}
|
||||
class="button is-secondary is-full-width-mobile">
|
||||
<span class="text">Update billing details</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<HeaderAlert title="Your projects are at risk">
|
||||
<svelte:fragment>
|
||||
{#if daysPassed > 30}
|
||||
Your scheduled payment on <b>{toLocaleDate($failedInvoice?.dueAt)}</b> failed.
|
||||
To resume write access of your organization, please update your billing details.
|
||||
{:else}
|
||||
Your scheduled payment on <b>{toLocaleDate($failedInvoice?.dueAt)}</b> failed.
|
||||
Access to paid projects within this organization will be disabled if no action
|
||||
is taken within 30 days.
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
href={`${base}/console/organization-${$failedInvoice?.teamId}/billing#paymentMethods`}
|
||||
secondary
|
||||
fullWidthMobile>
|
||||
<span class="text">Update billing details</span>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
<!-- TODO: add variable from backend that indicates org is due to be deleted -->
|
||||
{#if false}
|
||||
<HeaderAlert title="Organization flagged for deletion">
|
||||
<svelte:fragment>
|
||||
All existing projects in the {$organization.name} organization have been paused.
|
||||
This organization will be deleted once your upcoming invoice is processed successfully.
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Header slot="header" />
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
changeOrganizationFinalAction,
|
||||
changeOrganizationTier,
|
||||
changeTierSteps,
|
||||
currentBillingAddress,
|
||||
isUpgrade
|
||||
} from './wizard/cloudOrganizationChangeTier/store';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
@@ -24,12 +23,13 @@
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import deepEqual from 'deep-equal';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
async function onFinish() {
|
||||
await invalidate(Dependencies.FUNCTIONS);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
}
|
||||
|
||||
async function changeTier() {
|
||||
@@ -40,8 +40,13 @@
|
||||
$changeOrganizationTier.billingPlan,
|
||||
$changeOrganizationTier.paymentMethodId
|
||||
);
|
||||
feedback.submitFeedback(
|
||||
'downgrade',
|
||||
$changeOrganizationTier.feedbackMessage,
|
||||
$user?.name ?? '',
|
||||
$user.email
|
||||
);
|
||||
|
||||
//TODO: send feedback
|
||||
addNotification({
|
||||
type: 'success',
|
||||
isHtml: true,
|
||||
@@ -75,10 +80,14 @@
|
||||
);
|
||||
|
||||
//Add billing address
|
||||
if (
|
||||
if ($changeOrganizationTier.billingAddressId) {
|
||||
await sdk.forConsole.billing.setBillingAddress(
|
||||
org.$id,
|
||||
$changeOrganizationTier.billingAddressId
|
||||
);
|
||||
} else if (
|
||||
$changeOrganizationTier.billingAddress &&
|
||||
$changeOrganizationTier.billingAddress.streetAddress &&
|
||||
!deepEqual($changeOrganizationTier.billingAddress, $currentBillingAddress)
|
||||
$changeOrganizationTier.billingAddress.streetAddress
|
||||
) {
|
||||
const response = await sdk.forConsole.billing.createAddress(
|
||||
$changeOrganizationTier.billingAddress.country,
|
||||
@@ -94,6 +103,14 @@
|
||||
await sdk.forConsole.billing.setBillingAddress(org.$id, response.$id);
|
||||
}
|
||||
|
||||
//Add coupon
|
||||
if ($changeOrganizationTier.couponCode) {
|
||||
await sdk.forConsole.billing.addCredit(
|
||||
org.$id,
|
||||
$changeOrganizationTier.couponCode
|
||||
);
|
||||
}
|
||||
|
||||
//Add budget
|
||||
if ($changeOrganizationTier?.billingBudget) {
|
||||
await sdk.forConsole.billing.updateBudget(
|
||||
@@ -165,6 +182,7 @@
|
||||
billingPlan: 'tier-1',
|
||||
paymentMethodId: null,
|
||||
collaborators: [],
|
||||
billingAddressId: null,
|
||||
billingAddress: {
|
||||
$id: null,
|
||||
streetAddress: null,
|
||||
@@ -174,7 +192,8 @@
|
||||
postalCode: null,
|
||||
country: null
|
||||
},
|
||||
taxId: null
|
||||
taxId: null,
|
||||
feedbackMessage: null
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,12 @@
|
||||
$createOrganization.paymentMethodId
|
||||
);
|
||||
//Add billing address
|
||||
if (
|
||||
if ($createOrganization.billingAddressId) {
|
||||
await sdk.forConsole.billing.setBillingAddress(
|
||||
org.$id,
|
||||
$createOrganization.billingAddressId
|
||||
);
|
||||
} else if (
|
||||
$createOrganization.billingAddress &&
|
||||
$createOrganization.billingAddress.streetAddress
|
||||
) {
|
||||
@@ -106,6 +111,7 @@
|
||||
billingPlan: 'tier-1',
|
||||
paymentMethodId: null,
|
||||
collaborators: [],
|
||||
billingAddressId: null,
|
||||
billingAddress: {
|
||||
$id: null,
|
||||
streetAddress: null,
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { isStripeInitialized, stripe } from '$lib/stores/stripe';
|
||||
import { initializeStripe, isStripeInitialized, stripe } from '$lib/stores/stripe';
|
||||
import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
$: defaultPaymentMethod = $paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
|
||||
@@ -27,25 +29,58 @@
|
||||
|
||||
onMount(async () => {
|
||||
if (
|
||||
$isStripeInitialized &&
|
||||
$page.url.searchParams.has('invoice') &&
|
||||
$page.url.searchParams.has('type') &&
|
||||
$page.url.searchParams.get('type') === 'confirmation'
|
||||
($page.url.searchParams.has('invoice') &&
|
||||
$page.url.searchParams.has('type') &&
|
||||
$page.url.searchParams.get('type') === 'confirmation') ||
|
||||
$page.url.searchParams.has('clientSecret')
|
||||
) {
|
||||
if (!$isStripeInitialized) {
|
||||
await initializeStripe();
|
||||
}
|
||||
console.log('test');
|
||||
console.log($page.url.searchParams.get('invoice'));
|
||||
|
||||
const { setupIntent, error } = await $stripe.confirmCardSetup(
|
||||
'{SETUP_INTENT_CLIENT_SECRET}',
|
||||
{
|
||||
payment_method: {
|
||||
id: $organization.paymentMethodId,
|
||||
try {
|
||||
const invoiceId = $page.url.searchParams.get('invoice');
|
||||
const invoice = await sdk.forConsole.billing.getInvoice(
|
||||
$page.params.organization,
|
||||
invoiceId
|
||||
);
|
||||
const { setupIntent, error } = await $stripe.confirmCardSetup(
|
||||
invoice.clientSecret,
|
||||
{
|
||||
payment_method: $organization.paymentMethodId,
|
||||
return_url: `${base}/console/organization-${$organization.$id}/billing`
|
||||
}
|
||||
);
|
||||
console.log(setupIntent);
|
||||
if (error) {
|
||||
console.log('Something went wrong');
|
||||
}
|
||||
);
|
||||
if (error) {
|
||||
console.log('Something went wrong');
|
||||
if (setupIntent.status === 'succeeded') {
|
||||
if (typeof setupIntent.payment_method === 'string') {
|
||||
await sdk.forConsole.billing.setOrganizationPaymentMethod(
|
||||
$page.params.organization,
|
||||
setupIntent.payment_method
|
||||
);
|
||||
} else {
|
||||
await sdk.forConsole.billing.setOrganizationPaymentMethod(
|
||||
$page.params.organization,
|
||||
setupIntent.payment_method.id
|
||||
);
|
||||
}
|
||||
addNotification({
|
||||
title: 'Success',
|
||||
message: 'Your payment method has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
title: 'Error',
|
||||
message:
|
||||
'There was an error processing your payment, try again later. If the problem persists, please contact support.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
|
||||
<TableCell title="status">
|
||||
<Pill
|
||||
danger={status === 'overdue'}
|
||||
success={status === 'paid'}
|
||||
danger={status === 'overdue' || status === 'failed'}
|
||||
success={status === 'paid' || status === 'succeeded'}
|
||||
warning={status === 'pending'}>
|
||||
{status}
|
||||
</Pill>
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
onMount(async () => {
|
||||
methods = await sdk.forConsole.billing.listPaymentMethods();
|
||||
|
||||
if (!$organization.paymentMethodId && !$organization.backupPaymentMethodId) {
|
||||
selectedPaymentMethodId = methods?.total ? methods.paymentMethods[0].$id : null;
|
||||
}
|
||||
selectedPaymentMethodId = isBackup
|
||||
? $organization.backupPaymentMethodId
|
||||
: $organization.paymentMethodId;
|
||||
});
|
||||
|
||||
// TODO: fix new payment method replacement
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (!selectedPaymentMethodId) {
|
||||
@@ -167,7 +169,8 @@
|
||||
secondary
|
||||
submit
|
||||
disabled={selectedPaymentMethodId ===
|
||||
(isBackup ? $organization.backupPaymentMethodId : $organization.paymentMethodId)}
|
||||
>Save</Button>
|
||||
(isBackup ? $organization.backupPaymentMethodId : $organization.paymentMethodId)}>
|
||||
Save
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</FakeModal>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
name || undefined
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
showCreate = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
|
||||
@@ -30,14 +30,15 @@
|
||||
});
|
||||
|
||||
function calculateExcess() {
|
||||
const totBandwidth = usage?.bandwidth?.length > 0 ? usage.bandwidth[0].value : 0;
|
||||
const totUsers = usage?.users?.length > 0 ? usage.users[0].value : 0;
|
||||
excess = {
|
||||
bandwidth:
|
||||
usage?.bandwidth?.[0] > plan.bandwidth ? usage?.bandwidth?.[0] - plan.bandwidth : 0,
|
||||
bandwidth: totBandwidth > plan.bandwidth ? totBandwidth - plan.bandwidth : 0,
|
||||
storage:
|
||||
usage?.storage[0] > sizeToBytes(plan.storage, 'GB')
|
||||
? usage.storage[0] - plan.storage
|
||||
: 0,
|
||||
users: usage?.users?.[0] > plan.users ? usage?.users?.[0] - plan.users : 0,
|
||||
users: totUsers > plan.users ? totUsers - plan.users : 0,
|
||||
executions:
|
||||
usage?.executions[0] > plan.executions ? usage.executions[0] - plan.executions : 0,
|
||||
members: members.total > plan.members ? members.total - (plan.members || Infinity) : 0
|
||||
@@ -56,17 +57,22 @@
|
||||
</p>
|
||||
<PlanExcess {excess} currentTier={$organization.billingPlan} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
show = false;
|
||||
goto(`/console/organization-${$organization.$id}/usage`);
|
||||
}}>View usage</Button>
|
||||
<div class="u-flex u-main-space-between u-width-full-line">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
show = false;
|
||||
goto(`/console/organization-${$organization.$id}/usage`);
|
||||
}}>View usage</Button>
|
||||
|
||||
<Button
|
||||
on:click={() => {
|
||||
show = false;
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
}}>Upgrade plan</Button>
|
||||
<Button
|
||||
on:click={() => {
|
||||
show = false;
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
}}>Upgrade plan</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ url, params, route }) => {
|
||||
export const load: PageLoad = async ({ url, params, route, depends }) => {
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { onMount } from 'svelte';
|
||||
import Delete from '../deleteOrganization.svelte';
|
||||
import Delete from './deleteOrganization.svelte';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
|
||||
let name: string;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export const load: PageLoad = async ({ depends }) => {
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
return {
|
||||
projects: await sdk.forConsole.projects.list()
|
||||
};
|
||||
};
|
||||
+25
-10
@@ -8,8 +8,14 @@
|
||||
import { base } from '$app/paths';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { projects } from '../store';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
|
||||
export let showDelete = false;
|
||||
let error: string = null;
|
||||
|
||||
//TODO: replace variable with value from method
|
||||
const TEMPORARY_SET_FOR_DELETION = true;
|
||||
|
||||
async function deleteOrg() {
|
||||
try {
|
||||
@@ -26,12 +32,9 @@
|
||||
goto(`${base}/console/onboarding`);
|
||||
}
|
||||
showDelete = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.OrganizationDelete);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.OrganizationDelete);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -40,13 +43,25 @@
|
||||
title="Delete organization"
|
||||
onSubmit={deleteOrg}
|
||||
bind:show={showDelete}
|
||||
bind:error
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p data-private>
|
||||
Are you sure you want to delete <b>{$organization.name}</b>? All projects ({$organization.total})
|
||||
and data associated with this organization will be deleted. This action is irreversible.
|
||||
</p>
|
||||
{#if TEMPORARY_SET_FOR_DELETION}
|
||||
<p data-private>
|
||||
The organization <b>{$organization.name}</b> will be flagged for deletion.
|
||||
</p>
|
||||
|
||||
<p data-private>
|
||||
All existing projects will be paused and the organization will be deleted once your
|
||||
upcoming invoice is processed on {toLocaleDate($organization.billingNextInvoiceDate)}.
|
||||
</p>
|
||||
{:else}
|
||||
<p data-private>
|
||||
Are you sure you want to delete <b>{$organization.name}</b>? All projects ({$projects.total})
|
||||
and data associated with this organization will be deleted. This action is irreversible.
|
||||
</p>
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
@@ -1,3 +1,6 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const showExcess = writable<boolean>(false);
|
||||
export const projects = derived(page, ($page) => $page.data?.projects as Models.ProjectList);
|
||||
|
||||
@@ -5,12 +5,35 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { createProject } from './store';
|
||||
import type { RegionList } from '$lib/sdk/billing';
|
||||
import type { Region, RegionList } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
let regions: RegionList;
|
||||
let prefs: Models.Preferences;
|
||||
onMount(async () => {
|
||||
regions = await sdk.forConsole.billing.listRegions();
|
||||
prefs = await sdk.forConsole.account.getPrefs();
|
||||
});
|
||||
|
||||
async function notifyRegion(selectedRegion: Region) {
|
||||
try {
|
||||
let newPrefs = { ...prefs };
|
||||
newPrefs.notifications = newPrefs.notifications ?? [];
|
||||
newPrefs.notifications = [...newPrefs.notifications, selectedRegion.$id];
|
||||
const response = await sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
prefs = response.prefs;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
isHtml: true,
|
||||
message: `You will be notified when <b>${selectedRegion.name}</b> region is available`
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
$: notifications = prefs?.notifications ?? [];
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
@@ -39,10 +62,17 @@
|
||||
flag={region.flag}
|
||||
name={region.name} />
|
||||
<p class:u-opacity-50={region.disabled}>{region.name}</p>
|
||||
<Pill button event="region_notify">
|
||||
<span class="icon-bell" aria-hidden="true" />
|
||||
<span class="text">Notify me</span>
|
||||
</Pill>
|
||||
{#if !notifications.includes(region.$id)}
|
||||
<Pill
|
||||
button
|
||||
event="region_notify"
|
||||
on:click={() => {
|
||||
notifyRegion(region);
|
||||
}}>
|
||||
<span class="icon-bell" aria-hidden="true" />
|
||||
<span class="text">Notify me</span>
|
||||
</Pill>
|
||||
{/if}
|
||||
{:else}
|
||||
<Flag
|
||||
width={40}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { VARS } from '$lib/system';
|
||||
|
||||
onDestroy(() => {
|
||||
$supportData = {
|
||||
@@ -25,7 +26,7 @@
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const response = await fetch('https://growth.appwrite.io/v1/support', {
|
||||
const response = await fetch(`https://${VARS.GROWTH_ENDPOINT}/v1/support`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, FormList, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { FormItem, FormList, InputRadio, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Alert } from '$lib/components';
|
||||
import { createOrganization } from './store';
|
||||
import type { AddressesList } from '$lib/sdk/billing';
|
||||
|
||||
let options = [
|
||||
{
|
||||
@@ -12,8 +12,11 @@
|
||||
label: 'United States'
|
||||
}
|
||||
];
|
||||
let addressList: AddressesList;
|
||||
|
||||
onMount(async () => {
|
||||
addressList = await sdk.forConsole.billing.listAddresses();
|
||||
$createOrganization.billingAddressId = addressList.billingAddresses?.[0]?.$id ?? null;
|
||||
const countryList = await sdk.forProject.locale.listCountries();
|
||||
options = countryList.countries.map((country) => {
|
||||
return {
|
||||
@@ -28,58 +31,97 @@
|
||||
<svelte:fragment slot="title">Billing address</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">Add a billing address for your organization.</svelte:fragment>
|
||||
|
||||
<Alert type="info">
|
||||
Depending on your location, your billing address might need to be in the same country
|
||||
associated with the payment method for your organization.
|
||||
</Alert>
|
||||
<FormList gap={16} class="u-margin-block-start-24">
|
||||
<InputText
|
||||
bind:value={$createOrganization.taxId}
|
||||
id="text"
|
||||
label="Tax ID"
|
||||
placeholder="Enter tax ID"
|
||||
optionalText="(optional)" />
|
||||
<InputSelect
|
||||
bind:value={$createOrganization.billingAddress.country}
|
||||
{options}
|
||||
label="Country or region"
|
||||
placeholder="Select country or region"
|
||||
id="country"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$createOrganization.billingAddress.streetAddress}
|
||||
id="address"
|
||||
label="Street address"
|
||||
placeholder="Enter street address"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$createOrganization.billingAddress.addressLine2}
|
||||
id="address2"
|
||||
label="Address line 2"
|
||||
placeholder="Unit number, floor, etc." />
|
||||
<InputText
|
||||
bind:value={$createOrganization.billingAddress.city}
|
||||
id="city"
|
||||
label="City or suburb"
|
||||
placeholder="Enter your city"
|
||||
required />
|
||||
<FormItem isMultiple>
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$createOrganization.billingAddress.state}
|
||||
id="state"
|
||||
label="State"
|
||||
placeholder="Enter your state"
|
||||
required />
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$createOrganization.billingAddress.postalCode}
|
||||
id="zip"
|
||||
label="Postal code"
|
||||
placeholder="Enter postal code"
|
||||
required />
|
||||
</FormItem>
|
||||
<p class="text u-margin-block-start-24"><b>Billing address</b></p>
|
||||
<FormList class="u-margin-block-start-8">
|
||||
<div class:boxes-wrapper={addressList?.total}>
|
||||
{#if addressList?.total}
|
||||
{#each addressList.billingAddresses as address}
|
||||
<div class="box">
|
||||
<InputRadio
|
||||
id={`address-${address.$id}`}
|
||||
value={address.$id}
|
||||
name="address"
|
||||
bind:group={$createOrganization.billingAddressId}>
|
||||
<div
|
||||
class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2"
|
||||
style="padding-inline:0.25rem">
|
||||
<p class="text">{address.streetAddress}</p>
|
||||
{#if address?.addressLine2}
|
||||
<p class="text">{address.addressLine2}</p>
|
||||
{/if}
|
||||
<p class="text">{address.city}</p>
|
||||
<p class="text">{address.state}</p>
|
||||
<p class="text">{address.postalCode}</p>
|
||||
<p class="text">{address.country}</p>
|
||||
</div>
|
||||
</InputRadio>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="box">
|
||||
{#if addressList?.total}
|
||||
<InputRadio
|
||||
id="address"
|
||||
value={null}
|
||||
name="address"
|
||||
bind:group={$createOrganization.billingAddressId}>
|
||||
<span style="padding-inline:0.25rem">Add new billing address</span>
|
||||
</InputRadio>
|
||||
{/if}
|
||||
{#if $createOrganization.billingAddressId === null}
|
||||
<FormList gap={16} class="u-margin-block-start-24">
|
||||
<InputText
|
||||
bind:value={$createOrganization.taxId}
|
||||
id="text"
|
||||
label="Tax ID"
|
||||
placeholder="Enter tax ID"
|
||||
optionalText="(optional)" />
|
||||
<InputSelect
|
||||
bind:value={$createOrganization.billingAddress.country}
|
||||
{options}
|
||||
label="Country or region"
|
||||
placeholder="Select country or region"
|
||||
id="country"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$createOrganization.billingAddress.streetAddress}
|
||||
id="address"
|
||||
label="Street address"
|
||||
placeholder="Enter street address"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$createOrganization.billingAddress.addressLine2}
|
||||
id="address2"
|
||||
label="Address line 2"
|
||||
placeholder="Unit number, floor, etc." />
|
||||
<InputText
|
||||
bind:value={$createOrganization.billingAddress.city}
|
||||
id="city"
|
||||
label="City or suburb"
|
||||
placeholder="Enter your city"
|
||||
required />
|
||||
<FormItem isMultiple>
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$createOrganization.billingAddress.state}
|
||||
id="state"
|
||||
label="State"
|
||||
placeholder="Enter your state"
|
||||
required />
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$createOrganization.billingAddress.postalCode}
|
||||
id="zip"
|
||||
label="Postal code"
|
||||
placeholder="Enter postal code"
|
||||
required />
|
||||
</FormItem>
|
||||
</FormList>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
|
||||
@@ -11,7 +11,8 @@ export const createOrganization = writable<{
|
||||
name: string;
|
||||
billingPlan: Tier;
|
||||
paymentMethodId: string;
|
||||
billingAddress: Address;
|
||||
billingAddressId: string;
|
||||
billingAddress?: Address;
|
||||
collaborators?: string[];
|
||||
billingBudget?: number;
|
||||
taxId?: string;
|
||||
@@ -21,6 +22,7 @@ export const createOrganization = writable<{
|
||||
billingPlan: 'tier-1',
|
||||
paymentMethodId: null,
|
||||
collaborators: [],
|
||||
billingAddressId: null,
|
||||
billingAddress: {
|
||||
$id: null,
|
||||
streetAddress: null,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, FormList, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { FormItem, FormList, InputRadio, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Alert } from '$lib/components';
|
||||
import { changeOrganizationTier, currentBillingAddress } from './store';
|
||||
import { changeOrganizationTier } from './store';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { deepClone } from '$lib/helpers/object';
|
||||
import type { AddressesList } from '$lib/sdk/billing';
|
||||
|
||||
let options = [
|
||||
{
|
||||
@@ -14,8 +13,13 @@
|
||||
label: 'United States'
|
||||
}
|
||||
];
|
||||
let addressList: AddressesList;
|
||||
|
||||
onMount(async () => {
|
||||
addressList = await sdk.forConsole.billing.listAddresses();
|
||||
$changeOrganizationTier.billingAddressId = $organization.billingAddressId
|
||||
? $organization.billingAddressId
|
||||
: addressList.billingAddresses?.[0]?.$id ?? null;
|
||||
const countryList = await sdk.forProject.locale.listCountries();
|
||||
options = countryList.countries.map((country) => {
|
||||
return {
|
||||
@@ -23,13 +27,6 @@
|
||||
label: country.name
|
||||
};
|
||||
});
|
||||
|
||||
if ($organization.billingAddressId) {
|
||||
$currentBillingAddress = await sdk.forConsole.billing.getAddress(
|
||||
$organization.billingAddressId
|
||||
);
|
||||
$changeOrganizationTier.billingAddress = deepClone($currentBillingAddress);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -37,58 +34,97 @@
|
||||
<svelte:fragment slot="title">Billing address</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">Add a billing address for your organization.</svelte:fragment>
|
||||
|
||||
<Alert type="info">
|
||||
Depending on your location, your billing address might need to be in the same country
|
||||
associated with the payment method for your organization.
|
||||
</Alert>
|
||||
<FormList gap={16} class="u-margin-block-start-24">
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.taxId}
|
||||
id="text"
|
||||
label="Tax ID"
|
||||
placeholder="Enter tax ID"
|
||||
optionalText="(optional)" />
|
||||
<InputSelect
|
||||
bind:value={$changeOrganizationTier.billingAddress.country}
|
||||
{options}
|
||||
label="Country or region"
|
||||
placeholder="Select country or region"
|
||||
id="country"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.billingAddress.streetAddress}
|
||||
id="address"
|
||||
label="Street address"
|
||||
placeholder="Enter street address"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.billingAddress.addressLine2}
|
||||
id="address2"
|
||||
label="Address line 2"
|
||||
placeholder="Unit number, floor, etc." />
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.billingAddress.city}
|
||||
id="city"
|
||||
label="City or suburb"
|
||||
placeholder="Enter your city"
|
||||
required />
|
||||
<FormItem isMultiple>
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$changeOrganizationTier.billingAddress.state}
|
||||
id="state"
|
||||
label="State"
|
||||
placeholder="Enter your state"
|
||||
required />
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$changeOrganizationTier.billingAddress.postalCode}
|
||||
id="zip"
|
||||
label="Postal code"
|
||||
placeholder="Enter postal code"
|
||||
required />
|
||||
</FormItem>
|
||||
<p class="text u-margin-block-start-24"><b>Billing address</b></p>
|
||||
<FormList class="u-margin-block-start-8">
|
||||
<div class:boxes-wrapper={addressList?.total}>
|
||||
{#if addressList?.total}
|
||||
{#each addressList.billingAddresses as address}
|
||||
<div class="box">
|
||||
<InputRadio
|
||||
id={`address-${address.$id}`}
|
||||
value={address.$id}
|
||||
name="address"
|
||||
bind:group={$changeOrganizationTier.billingAddressId}>
|
||||
<div
|
||||
class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2"
|
||||
style="padding-inline:0.25rem">
|
||||
<p class="text">{address.streetAddress}</p>
|
||||
{#if address?.addressLine2}
|
||||
<p class="text">{address.addressLine2}</p>
|
||||
{/if}
|
||||
<p class="text">{address.city}</p>
|
||||
<p class="text">{address.state}</p>
|
||||
<p class="text">{address.postalCode}</p>
|
||||
<p class="text">{address.country}</p>
|
||||
</div>
|
||||
</InputRadio>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="box">
|
||||
{#if addressList?.total}
|
||||
<InputRadio
|
||||
id="address"
|
||||
value={null}
|
||||
name="address"
|
||||
bind:group={$changeOrganizationTier.billingAddressId}>
|
||||
<span style="padding-inline:0.25rem">Add new billing address</span>
|
||||
</InputRadio>
|
||||
{/if}
|
||||
{#if $changeOrganizationTier.billingAddressId === null}
|
||||
<FormList gap={16} class="u-margin-block-start-24">
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.taxId}
|
||||
id="text"
|
||||
label="Tax ID"
|
||||
placeholder="Enter tax ID"
|
||||
optionalText="(optional)" />
|
||||
<InputSelect
|
||||
bind:value={$changeOrganizationTier.billingAddress.country}
|
||||
{options}
|
||||
label="Country or region"
|
||||
placeholder="Select country or region"
|
||||
id="country"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.billingAddress.streetAddress}
|
||||
id="address"
|
||||
label="Street address"
|
||||
placeholder="Enter street address"
|
||||
required />
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.billingAddress.addressLine2}
|
||||
id="address2"
|
||||
label="Address line 2"
|
||||
placeholder="Unit number, floor, etc." />
|
||||
<InputText
|
||||
bind:value={$changeOrganizationTier.billingAddress.city}
|
||||
id="city"
|
||||
label="City or suburb"
|
||||
placeholder="Enter your city"
|
||||
required />
|
||||
<FormItem isMultiple>
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$changeOrganizationTier.billingAddress.state}
|
||||
id="state"
|
||||
label="State"
|
||||
placeholder="Enter your state"
|
||||
required />
|
||||
<InputText
|
||||
isMultiple
|
||||
fullWidth
|
||||
bind:value={$changeOrganizationTier.billingAddress.postalCode}
|
||||
id="zip"
|
||||
label="Postal code"
|
||||
placeholder="Enter postal code"
|
||||
required />
|
||||
</FormItem>
|
||||
</FormList>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
|
||||
@@ -41,14 +41,15 @@
|
||||
(plan) => plan.$id === $changeOrganizationTier.billingPlan
|
||||
);
|
||||
|
||||
const totBandwidth = usage?.bandwidth?.length > 0 ? usage.bandwidth[0].value : 0;
|
||||
const totUsers = usage?.users?.length > 0 ? usage.users[0].value : 0;
|
||||
$changeOrganizationTier.limitOverflow = {
|
||||
bandwidth:
|
||||
usage.bandwidth[0] > plan.bandwidth ? usage.bandwidth[0] - plan.bandwidth : 0,
|
||||
bandwidth: totBandwidth > plan.bandwidth ? totBandwidth - plan.bandwidth : 0,
|
||||
storage:
|
||||
usage.storage[0] > sizeToBytes(plan.storage, 'GB')
|
||||
? usage.storage[0] - plan.storage
|
||||
: 0,
|
||||
users: usage.users[0] > plan.users ? usage.users[0] - plan.users : 0,
|
||||
users: totUsers > plan.users ? totUsers - plan.users : 0,
|
||||
executions:
|
||||
usage.executions[0] > plan.executions ? usage.executions[0] - plan.executions : 0,
|
||||
members: members.total > plan.members ? members.total - (plan.members || Infinity) : 0
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -21,6 +22,12 @@
|
||||
|
||||
let coupon: string = null;
|
||||
let comment: string = null;
|
||||
let couponData: Partial<Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
let showCoupon: boolean = false;
|
||||
async function fetchCard() {
|
||||
try {
|
||||
const card = await sdk.forConsole.billing.getPaymentMethod(
|
||||
@@ -32,15 +39,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function applyCredit() {
|
||||
async function addCoupon() {
|
||||
try {
|
||||
await sdk.forConsole.billing.getCoupon(coupon);
|
||||
const response = await sdk.forConsole.billing.getCoupon(coupon);
|
||||
coupon = null;
|
||||
couponData = response;
|
||||
showCoupon = true;
|
||||
$changeOrganizationTier.couponCode = response.$id;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
couponData.code = coupon;
|
||||
couponData.status = 'error';
|
||||
showCoupon = true;
|
||||
}
|
||||
}
|
||||
|
||||
function removeCoupon() {
|
||||
couponData = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
showCoupon = false;
|
||||
}
|
||||
|
||||
$: downgradeToStarter = $changeOrganizationTier.billingPlan === 'tier-0';
|
||||
$: if (!$isUpgrade) {
|
||||
$changeOrganizationFinalAction = 'Confirm plan change';
|
||||
@@ -55,7 +77,6 @@
|
||||
there is a specific reason you chose to change your plan at this time, please let us
|
||||
know.
|
||||
</svelte:fragment>
|
||||
<!-- TODO: submit feedback -->
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="comment"
|
||||
@@ -102,14 +123,40 @@
|
||||
{/if}
|
||||
|
||||
<Box class="u-margin-block-start-32 u-flex u-flex-vertical u-gap-16" radius="small">
|
||||
<FormList>
|
||||
<FormList gap={8}>
|
||||
<InputText
|
||||
placeholder="Coupon code"
|
||||
id="code"
|
||||
label="Add credits"
|
||||
disabled={showCoupon && couponData?.status !== 'error'}
|
||||
bind:value={coupon}>
|
||||
<Button secondary disabled={!coupon} on:click={applyCredit}>Apply</Button>
|
||||
<Button
|
||||
secondary
|
||||
disabled={(showCoupon && couponData?.status !== 'error') || !coupon}
|
||||
on:click={addCoupon}>
|
||||
Apply
|
||||
</Button>
|
||||
</InputText>
|
||||
{#if showCoupon}
|
||||
{#if couponData?.status === 'error'}
|
||||
<div>
|
||||
<span class="icon-exclamation-circle u-color-text-danger" />
|
||||
<span>
|
||||
{couponData.code} is not a valid promo code
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<div>
|
||||
<span class="icon-tag u-color-text-success" />
|
||||
<span>
|
||||
{couponData.code} applied (-${couponData.credits})
|
||||
</span>
|
||||
</div>
|
||||
<Button text on:click={removeCoupon}>Remove</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</FormList>
|
||||
{#if $changeOrganizationTier.billingPlan !== 'tier-0'}
|
||||
<span class="u-flex u-main-space-between">
|
||||
|
||||
@@ -11,7 +11,8 @@ export const changeOrganizationTier = writable<{
|
||||
id?: string;
|
||||
billingPlan: Tier;
|
||||
paymentMethodId: string;
|
||||
billingAddress: Address;
|
||||
billingAddressId: string;
|
||||
billingAddress?: Address;
|
||||
billingBudget?: number;
|
||||
collaborators?: string[];
|
||||
isOverLimit?: boolean;
|
||||
@@ -23,12 +24,15 @@ export const changeOrganizationTier = writable<{
|
||||
members?: number;
|
||||
};
|
||||
taxId?: string;
|
||||
feedbackMessage?: string;
|
||||
couponCode?: string;
|
||||
}>({
|
||||
id: null,
|
||||
billingPlan: 'tier-1',
|
||||
paymentMethodId: null,
|
||||
collaborators: [],
|
||||
isOverLimit: false,
|
||||
billingAddressId: null,
|
||||
billingAddress: {
|
||||
$id: null,
|
||||
streetAddress: null,
|
||||
@@ -40,13 +44,3 @@ export const changeOrganizationTier = writable<{
|
||||
},
|
||||
taxId: null
|
||||
});
|
||||
|
||||
export const currentBillingAddress = writable<Address>({
|
||||
$id: null,
|
||||
streetAddress: null,
|
||||
addressLine2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postalCode: null,
|
||||
country: null
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user