mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: billing address, and many fixes
This commit is contained in:
@@ -227,6 +227,8 @@ export enum Submit {
|
||||
OrganizationBackupPaymentAdded = 'submit_organization_backup_payment_added',
|
||||
OrganizationBackupPaymentUpdated = 'submit_organization_backup_payment_updated',
|
||||
OrganizationBackupPaymentRemoved = 'submit_organization_backup_payment_removed',
|
||||
OrganizationBillingAddressAdded = 'submit_organization_billing_address_added',
|
||||
OrganizationBillingAddressRemoved = 'submit_organization_billing_address_removed',
|
||||
PromoCodeRedeemed = 'submit_promo_code_redeemed',
|
||||
SupportTicket = 'submit_support_ticket'
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
await goto(href);
|
||||
await waitUntil(() => {
|
||||
console.log('tickUntil', el);
|
||||
return el.classList.contains('is-selected');
|
||||
}, 1000);
|
||||
el.focus();
|
||||
|
||||
@@ -5,6 +5,7 @@ export const INTERVAL = 5 * 60000; // default interval to check for feedback
|
||||
export enum Dependencies {
|
||||
CREDIT = 'dependency:credit',
|
||||
INVOICES = 'dependency:invoices',
|
||||
ADDRESS = 'dependency:address',
|
||||
PAYMENT_METHODS = 'dependency:paymentMethods',
|
||||
ORGANIZATION = 'dependency:organization',
|
||||
PROJECT = 'dependency:project',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Client, Query } from '@appwrite.io/console';
|
||||
import type { Organization } from '../stores/organization';
|
||||
import type { PaymentMethod } from '@stripe/stripe-js';
|
||||
import type BillingAddress from '$routes/console/account/payments/billingAddress.svelte';
|
||||
|
||||
export type PaymentMethodData = {
|
||||
$id: string;
|
||||
@@ -161,7 +160,7 @@ export type Address = {
|
||||
};
|
||||
|
||||
export type AddressesList = {
|
||||
addresses: Address[];
|
||||
billingAddresses: Address[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
@@ -197,6 +196,11 @@ export type Plan = {
|
||||
realtimeAddon: AdditionalResource | null;
|
||||
};
|
||||
|
||||
export type PlanList = {
|
||||
plans: Plan[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export class Billing {
|
||||
client: Client;
|
||||
|
||||
@@ -515,7 +519,7 @@ export class Billing {
|
||||
async setBillingAddress(
|
||||
organizationId: string,
|
||||
billingAddressId: string
|
||||
): Promise<BillingAddress> {
|
||||
): Promise<Organization> {
|
||||
const path = `/organizations/${organizationId}/billing-address`;
|
||||
const params = {
|
||||
organizationId,
|
||||
@@ -532,7 +536,7 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async removeBillingAddress(organizationId: string): Promise<BillingAddress> {
|
||||
async removeBillingAddress(organizationId: string): Promise<void> {
|
||||
const path = `/organizations/${organizationId}/billing-address`;
|
||||
const params = {
|
||||
organizationId
|
||||
@@ -787,7 +791,7 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async getPlans(): Promise<RegionList> {
|
||||
async getPlanList(): Promise<PlanList> {
|
||||
const path = `/console/plans`;
|
||||
const params = {};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
|
||||
@@ -6,22 +6,32 @@
|
||||
import { Button, InputNumber, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let show = false;
|
||||
|
||||
//TODO: fetch countries from appwrite
|
||||
const options = [
|
||||
{
|
||||
value: 'US',
|
||||
label: 'United States'
|
||||
}
|
||||
];
|
||||
let country: string;
|
||||
let address: string;
|
||||
let address2: string;
|
||||
let city: string;
|
||||
let state: string;
|
||||
let zip: number;
|
||||
let options = [
|
||||
{
|
||||
value: 'US',
|
||||
label: 'United States'
|
||||
}
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const countryList = await sdk.forProject.locale.listCountries();
|
||||
options = countryList.countries.map((country) => {
|
||||
return {
|
||||
value: country.code,
|
||||
label: country.name
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
@@ -33,19 +43,19 @@
|
||||
state,
|
||||
zip?.toString()
|
||||
);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
await invalidate(Dependencies.ADDRESS);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Address has been deleted`
|
||||
message: `Address has been added`
|
||||
});
|
||||
trackEvent(Submit.BillingAddressDeleted);
|
||||
trackEvent(Submit.BillingAddressCreated);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.BillingAddressDeleted);
|
||||
trackError(error, Submit.BillingAddressCreated);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,6 @@
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Save</Button>
|
||||
<Button submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -50,6 +50,6 @@
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Save</Button>
|
||||
<Button submit disabled={!name}>Save</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
import { failedInvoice, paymentMethods } from '$lib/stores/billing';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
|
||||
$: defaultPaymentMethod = $paymentMethods.paymentMethods.find(
|
||||
(method: PaymentMethodData) => method.$id === $organization.paymentMethodId
|
||||
$: defaultPaymentMethod = $paymentMethods?.paymentMethods?.find(
|
||||
(method: PaymentMethodData) => method.$id === $organization?.paymentMethodId
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
await sdk.forConsole.billing.addCredit($organization.$id, coupon);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `A new payment method has been added to ${$organization.name}`
|
||||
message: `Credit has been added to ${$organization.name}`
|
||||
});
|
||||
invalidate(Dependencies.ORGANIZATION);
|
||||
trackEvent(Submit.CouponRedeemed, {
|
||||
@@ -55,6 +55,7 @@
|
||||
}
|
||||
|
||||
async function request() {
|
||||
if (!$organization?.$id) return;
|
||||
creditList = await sdk.forConsole.billing.listCredits($organization.$id, [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset)
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, DropList, Heading } from '$lib/components';
|
||||
import DropListItem from '$lib/components/dropListItem.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import AddressModal from '$routes/console/account/payments/addressModal.svelte';
|
||||
import { addressList } from './store';
|
||||
|
||||
let showDropdown = false;
|
||||
let showCreate = false;
|
||||
|
||||
async function addAddress(addressId: string) {
|
||||
try {
|
||||
await sdk.forConsole.billing.setBillingAddress($organization.$id, addressId);
|
||||
await invalidate(Dependencies.ADDRESS);
|
||||
showDropdown = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `A new billing address has been added to ${$organization.name}`
|
||||
});
|
||||
trackEvent(Submit.OrganizationBillingAddressAdded);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.OrganizationBillingAddressAdded);
|
||||
}
|
||||
}
|
||||
|
||||
$: console.log($addressList);
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
@@ -21,18 +51,24 @@
|
||||
<i class="icon-plus" />
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<!-- {#if $addressList.total}
|
||||
{#each $addressList.addresses as address}
|
||||
<DropListItem
|
||||
on:click={() => addAddress(address?.$id)}>
|
||||
{#if $addressList?.total}
|
||||
{#each $addressList.billingAddresses as address}
|
||||
<DropListItem on:click={() => addAddress(address?.$id)}>
|
||||
<p class="text">
|
||||
{address.address1}
|
||||
<span>{address.city}</span>
|
||||
<span>{address.streetAddress}</span>,
|
||||
<span>{address.city}</span>,
|
||||
<span>{address.state}</span>,
|
||||
<span>{address.postalCode}</span>,
|
||||
<span>{address.country}</span>
|
||||
</p>
|
||||
</DropListItem>
|
||||
{/each}
|
||||
{/if} -->
|
||||
<DropListItem>Add new billing address</DropListItem>
|
||||
{/if}
|
||||
<DropListItem
|
||||
on:click={() => {
|
||||
showCreate = true;
|
||||
showDropdown = false;
|
||||
}}>Add new billing address</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
@@ -43,3 +79,5 @@
|
||||
</article>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<AddressModal bind:show={showCreate} />
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import DeleteOrgPayment from './deleteOrgPayment.svelte';
|
||||
import ReplaceCard from './replaceCard.svelte';
|
||||
import EditPaymentModal from '$routes/console/account/payments/editPaymentModal.svelte';
|
||||
|
||||
let showDropdown = false;
|
||||
let showDropdownBackup = false;
|
||||
|
||||
let showPayment = false;
|
||||
let showEdit = false;
|
||||
let showDelete = false;
|
||||
let showReplace = false;
|
||||
let isSelectedBackup = false;
|
||||
@@ -65,13 +67,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($organization.backupPaymentMethodId) {
|
||||
$: if ($organization?.backupPaymentMethodId) {
|
||||
sdk.forConsole.billing
|
||||
.getPaymentMethod($organization.backupPaymentMethodId)
|
||||
.then((res) => (backupPaymentMethod = res));
|
||||
}
|
||||
|
||||
$: if ($organization.paymentMethodId) {
|
||||
$: if ($organization?.paymentMethodId) {
|
||||
sdk.forConsole.billing
|
||||
.getPaymentMethod($organization.paymentMethodId)
|
||||
.then((res) => (defaultPaymentMethod = res));
|
||||
@@ -107,7 +109,8 @@
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
console.log('test');
|
||||
showEdit = true;
|
||||
showDropdown = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
@@ -189,7 +192,9 @@
|
||||
<DropListItem
|
||||
icon="pencil"
|
||||
on:click={() => {
|
||||
console.log('test');
|
||||
showEdit = true;
|
||||
isSelectedBackup = true;
|
||||
showDropdownBackup = false;
|
||||
}}>
|
||||
Edit
|
||||
</DropListItem>
|
||||
@@ -247,8 +252,9 @@
|
||||
</DropListItem>
|
||||
{/each}
|
||||
{/if}
|
||||
<DropListItem on:click={() => (showPayment = true)}
|
||||
>Add new payment method</DropListItem>
|
||||
<DropListItem on:click={() => (showPayment = true)}>
|
||||
Add new payment method
|
||||
</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</div>
|
||||
@@ -264,6 +270,13 @@
|
||||
{#if showPayment && isCloud && hasStripePublicKey}
|
||||
<PaymentModal bind:show={showPayment} />
|
||||
{/if}
|
||||
{#if showEdit && isCloud && hasStripePublicKey}
|
||||
<EditPaymentModal
|
||||
selectedPaymentMethod={isSelectedBackup
|
||||
? $organization.backupPaymentMethodId
|
||||
: $organization.paymentMethodId}
|
||||
bind:show={showEdit} />
|
||||
{/if}
|
||||
{#if showReplace && isCloud && hasStripePublicKey}
|
||||
<ReplaceCard bind:show={showReplace} isBackup={isSelectedBackup} />
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { AddressesList } from '$lib/sdk/billing';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export const addressList = derived(page, ($page) => $page.data.addressList as string | null);
|
||||
export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList);
|
||||
export const aggregationList = derived(
|
||||
page,
|
||||
($page) => $page.data.aggregationList as string | null
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { usageRates } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
@@ -12,14 +11,50 @@
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { onMount } from 'svelte';
|
||||
import { createOrganization } from './store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
|
||||
export let show = false;
|
||||
export let tier: string;
|
||||
|
||||
let plan: Plan = null;
|
||||
onMount(async () => {
|
||||
const planList = await sdk.forConsole.billing.getPlanList();
|
||||
plan = planList.plans.find((p) => p.$id === tier);
|
||||
});
|
||||
|
||||
$: nextDate = $createOrganization?.name
|
||||
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
|
||||
: $organization?.billingNextInvoiceDate;
|
||||
|
||||
$: console.log(plan);
|
||||
|
||||
const planData = [
|
||||
{
|
||||
id: 'members',
|
||||
resource: 'Organization members',
|
||||
unit: ''
|
||||
},
|
||||
{ id: 'bandwith', resource: 'Bandwidth', unit: 'GB' },
|
||||
{ id: 'storage', resource: 'Storage', unit: 'GB' },
|
||||
{
|
||||
id: 'executions',
|
||||
resource: 'Function executions',
|
||||
unit: 'executions'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
resource: 'Active users',
|
||||
unit: 'AU'
|
||||
},
|
||||
{
|
||||
id: 'realtime',
|
||||
resource: 'Concurrent connections',
|
||||
unit: 'connections'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="big" headerDivider={false}>
|
||||
@@ -36,11 +71,11 @@
|
||||
<TableCellHead>Rate</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each usageRates[tier] as usage}
|
||||
{#each planData as usage}
|
||||
<TableRow>
|
||||
<TableCellText title="resource">{usage.resource}</TableCellText>
|
||||
<TableCellText title="limit">{usage.amount}{usage?.unit}</TableCellText>
|
||||
<TableCellText title="rate">{usage.rate}</TableCellText>
|
||||
<TableCellText title="limit">{plan[usage.id]}{usage?.unit}</TableCellText>
|
||||
<TableCellText title="rate">{plan[`${usage.id}Addon`]}</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
|
||||
@@ -96,5 +96,5 @@
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
{#if $organization.billingPlan === 'tier-0'}TODO: add transfer modal{/if}
|
||||
<!-- {#if $organization.billingPlan === 'tier-0'}TODO: add transfer modal{/if} -->
|
||||
</WizardStep>
|
||||
|
||||
Reference in New Issue
Block a user