mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: org creation, payment page WIP
This commit is contained in:
@@ -34,6 +34,12 @@
|
||||
title: 'Organizations',
|
||||
event: 'organizations',
|
||||
hasChildren: true
|
||||
},
|
||||
{
|
||||
href: `${path}/payments`,
|
||||
title: 'Payments details',
|
||||
event: 'payments',
|
||||
hasChildren: true
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import AvailableCredit from './availableCredit.svelte';
|
||||
import BillingAddress from './billingAddress.svelte';
|
||||
import PaymentHistory from './paymentHistory.svelte';
|
||||
import PaymentMethods from './paymentMethods.svelte';
|
||||
import PaymentModal from './paymentModal.svelte';
|
||||
import PaymentSummary from './paymentSummary.svelte';
|
||||
|
||||
let showPayment = false;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<PaymentSummary />
|
||||
<PaymentHistory />
|
||||
<PaymentMethods bind:showPayment />
|
||||
<BillingAddress />
|
||||
<AvailableCredit />
|
||||
</Container>
|
||||
|
||||
{#if showPayment}
|
||||
<PaymentModal bind:show={showPayment} />
|
||||
{/if}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params, parent, depends }) => {
|
||||
await parent();
|
||||
depends(Dependencies.PAYMENT_METHODS);
|
||||
|
||||
return {
|
||||
// paymentMethods: await sdk.forConsole.billing.listPaymentMethods()
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputText } from '$lib/elements/forms';
|
||||
|
||||
let credit = 0;
|
||||
let coupon: string = null;
|
||||
|
||||
async function redeem() {
|
||||
const response = await fetch('/v1/billing/credit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
coupon
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
credit = data.sum;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="6">Available Credit</Heading>
|
||||
|
||||
<p class="text">Appwrite credit will automatically be applied to your next invoice.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<p class="text u-bold">Credit balance: <span class="u-success">{credit}</span></p>
|
||||
|
||||
<FormList>
|
||||
<InputText
|
||||
placeholder="Coupon code"
|
||||
id="coupon"
|
||||
label="Add credit"
|
||||
bind:value={coupon} />
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={redeem} event="redeem_code">Redeem</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Empty, Heading } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="6">Billing Address</Heading>
|
||||
|
||||
<p class="text">
|
||||
View or update your billing address. This address will appear on your invoice.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Empty>
|
||||
<p class="text">Add a billing address</p>
|
||||
</Empty>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, EmptySearch, Heading } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="6">Payment History</Heading>
|
||||
|
||||
<p class="text">
|
||||
Transaction history for this Organization. Download invoices for more details about your
|
||||
payments.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<EmptySearch cardOnly>
|
||||
<p class="text u-text-center">
|
||||
You have no payment history. After you receive your first invoice, you'll see it
|
||||
here.
|
||||
</p>
|
||||
</EmptySearch>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Empty, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { paymentMethods } from './store';
|
||||
export let showPayment = false;
|
||||
|
||||
console.log($paymentMethods);
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="6">Payment Methods</Heading>
|
||||
|
||||
<p class="text">View or update your organization payment methods here.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $paymentMethods.total > 0}
|
||||
{#each $paymentMethods.paymentMethods as paymentMethod}
|
||||
<p>
|
||||
{paymentMethod.brand}
|
||||
{paymentMethod.last4}, expiring: {paymentMethod.expiryMonth}/{paymentMethod.expiryYear}
|
||||
{#if paymentMethod.default} <span class="icon-check-circle" /> {/if}
|
||||
</p>
|
||||
{/each}
|
||||
<Button text noMargin on:click={() => (showPayment = true)}>
|
||||
<span class="icon-plus" />
|
||||
<span class="text">Add a payment method</span>
|
||||
</Button>
|
||||
{:else}
|
||||
<Empty on:click={() => (showPayment = true)}>
|
||||
<p class="text">Add a payment method</p>
|
||||
</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
// import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal } from '$lib/components';
|
||||
import { InputText, Button, FormList } from '$lib/elements/forms';
|
||||
import { paymentMethods, publicKey } from './store';
|
||||
import {
|
||||
loadStripe,
|
||||
type Stripe,
|
||||
type StripeElements,
|
||||
type PaymentMethod
|
||||
} from '@stripe/stripe-js';
|
||||
import { onMount } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { apperanceDark, apperanceLight } from '$lib/stores/billing';
|
||||
|
||||
export let show = false;
|
||||
|
||||
let name: string;
|
||||
let error: string;
|
||||
let elements: StripeElements;
|
||||
let stripe: Stripe;
|
||||
|
||||
let clientSecret: string;
|
||||
let paymentMethod: PaymentMethod;
|
||||
|
||||
onMount(async () => {
|
||||
stripe = await loadStripe(publicKey);
|
||||
try {
|
||||
clientSecret = $paymentMethods?.paymentMethods[0]?.clientSecret;
|
||||
if (!clientSecret) {
|
||||
paymentMethod = await sdk.forConsole.billing.createPaymentMethod($organization.$id);
|
||||
}
|
||||
const options = {
|
||||
clientSecret: clientSecret ? clientSecret : paymentMethod.clientSecret,
|
||||
appearance: $app.themeInUse === 'dark' ? apperanceDark : apperanceLight
|
||||
};
|
||||
console.log(clientSecret);
|
||||
// Set up Stripe.js and Elements to use in checkout form, passing the client secret obtained in step 3
|
||||
elements = stripe.elements(options);
|
||||
createForm();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
});
|
||||
|
||||
async function createForm() {
|
||||
const paymentElement = elements.create('payment');
|
||||
paymentElement.mount('#payment-element');
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await stripe.confirmSetup({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: 'http://localhost:3000'
|
||||
},
|
||||
redirect: 'if_required'
|
||||
});
|
||||
paymentMethod = await sdk.forConsole.billing.createPaymentMethod($organization.$id);
|
||||
const { setupIntent } = await stripe.retrieveSetupIntent(paymentMethod.clientSecret);
|
||||
if (setupIntent && setupIntent.status === 'succeeded') {
|
||||
await sdk.forConsole.billing.updatePaymentMethod(
|
||||
$organization.$id,
|
||||
paymentMethod.$id,
|
||||
setupIntent.payment_method as string
|
||||
);
|
||||
await invalidate(Dependencies.PAYMENT_METHODS);
|
||||
show = false;
|
||||
console.log('test');
|
||||
// const paymentElement = elements.getElement('payment');
|
||||
// paymentElement.destroy();
|
||||
} else console.log('test2');
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
console.log(e);
|
||||
// trackError(StripeError, Submit.ProjectCreate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {error} onSubmit={handleSubmit} size="big" bind:show>
|
||||
<svelte:fragment slot="header">Add Payment Method</svelte:fragment>
|
||||
This payment method will be added to the {$organization.name} billing details.
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Cardholder name"
|
||||
placeholder="Cardholder name"
|
||||
bind:value={name}
|
||||
required
|
||||
autofocus={true} />
|
||||
|
||||
<div id="payment-element">
|
||||
<!-- Elements will create form elements here -->
|
||||
</div>
|
||||
<p class="text u-small">
|
||||
You won't be charged immediately. You will be charged for your plan on the first day of
|
||||
each month using the payment method you've specified above.
|
||||
</p>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Save</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Box, CardGrid, Heading } from '$lib/components';
|
||||
import { VARS } from '$lib/system';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="6">Payment Summary</Heading>
|
||||
|
||||
<p class="text">An overview of your upcoming payments.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">PLACEHOLDER PLAN</h6>
|
||||
</svelte:fragment>
|
||||
<p class="u-trim-1">X projects</p>
|
||||
</Box>
|
||||
{#if VARS.CONSOLE_TIER && VARS.CONSOLE_TIER !== 'base'}
|
||||
<div class="u-flex">
|
||||
<p class="u-bold">Next billing data</p>
|
||||
<p class="text u-margin-inline-start-auto">XX DATE</p>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,6 @@
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export const publicKey = import.meta.env?.VITE_STRIPE_PUBLIC_KEY?.toString() as string | undefined;
|
||||
|
||||
export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods);
|
||||
@@ -78,5 +78,6 @@
|
||||
<Wizard
|
||||
title="Create organization "
|
||||
steps={stepsComponents}
|
||||
finalAction="Start trial"
|
||||
on:finish={create}
|
||||
on:exit={onFinish} />
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
<FormList>
|
||||
<div class:boxes-wrapper={methods?.length}>
|
||||
{#each methods as method}
|
||||
<div class="box">test</div>
|
||||
<InputRadio
|
||||
id="payment-method"
|
||||
label="Payment method"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Alert } from '$lib/components';
|
||||
import { Button, FormList, InputEmail } from '$lib/elements/forms';
|
||||
import { Button, Form, FormList, InputEmail } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -44,7 +44,8 @@
|
||||
|
||||
<Alert type="info">
|
||||
{#if $createOrganization.tier === 'pro'}
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Architecto, quasi?
|
||||
You can add unlimited organization members on the Pro plan for $20 each. Each member
|
||||
added will receive an email invite to your organization on completion.
|
||||
{:else if $createOrganization.tier === 'starter'}
|
||||
You can add up to three organization members with the Starter plan for $10 each. Each
|
||||
member added will receive an email invite to your organization on completion.
|
||||
@@ -54,45 +55,55 @@
|
||||
{/if}
|
||||
</Alert>
|
||||
|
||||
<FormList>
|
||||
<InputEmail
|
||||
label="Email address"
|
||||
id="email"
|
||||
placeholder="Email address"
|
||||
bind:value={email}
|
||||
required />
|
||||
<Button secondary on:click={addCollaborator}>Add</Button>
|
||||
</FormList>
|
||||
<div class="u-margin-block-start-24">
|
||||
<Form onSubmit={addCollaborator}>
|
||||
<FormList>
|
||||
<InputEmail
|
||||
label="Email address"
|
||||
id="email"
|
||||
placeholder="Email address"
|
||||
bind:value={email}
|
||||
required />
|
||||
<Button secondary submit>Add</Button>
|
||||
</FormList>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{#if $createOrganization?.collaborators?.length}
|
||||
<Table noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>Collaborator</TableCellHead>
|
||||
<TableCellHead>Cost</TableCellHead>
|
||||
<TableCellHead>Admin</TableCellHead>
|
||||
<TableCellHead />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each $createOrganization.collaborators as collaborator}
|
||||
<TableRow>
|
||||
<TableCellText title="collaborator">{collaborator.email}</TableCellText>
|
||||
<TableCellText title="cost">PRICE</TableCellText>
|
||||
<TableCell>
|
||||
<input type="checkbox" name="role" id={collaborator.email} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="remove collaborator"
|
||||
on:click={removeCollaborator(collaborator.email)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-margin-block-start-24">
|
||||
<Table noStyles noMargin>
|
||||
<TableHeader>
|
||||
<TableCellHead>Collaborator</TableCellHead>
|
||||
<TableCellHead>Cost</TableCellHead>
|
||||
<TableCellHead>Admin</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each $createOrganization.collaborators as collaborator}
|
||||
<TableRow>
|
||||
<TableCellText title="collaborator">{collaborator.email}</TableCellText>
|
||||
<TableCellText title="cost">PRICE</TableCellText>
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="role"
|
||||
id={collaborator.email}
|
||||
bind:checked={collaborator.role} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="remove collaborator"
|
||||
on:click={() => removeCollaborator(collaborator.email)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{/if}
|
||||
</WizardStep>
|
||||
|
||||
@@ -1,94 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { CustomId, LabelCard } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { InputText, FormList } from '$lib/elements/forms';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { organizationList } from '$lib/stores/organization';
|
||||
import { createOrganization } from './store';
|
||||
|
||||
let showCustomId = false;
|
||||
|
||||
$: anyOrgFree = $organizationList.teams?.find((org) => org?.tier === 'free');
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Organization Details</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
label="Name"
|
||||
id="name"
|
||||
placeholder="Organization name"
|
||||
bind:value={$createOrganization.name}
|
||||
required />
|
||||
<svelte:fragment slot="title">Confirm your details</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle"
|
||||
>Confirm the details of your new organization and start your free trial.
|
||||
</svelte:fragment>
|
||||
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}>
|
||||
<span class="icon-pencil" aria-hidden="true" />
|
||||
<span class="text">Organization ID </span>
|
||||
</Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId
|
||||
bind:show={showCustomId}
|
||||
name="Organization"
|
||||
bind:id={$createOrganization.id} />
|
||||
<p class="body-text-1 u-bold">Organization name</p>
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<p class="text">{$createOrganization.name}</p>
|
||||
{#if $createOrganization?.id}
|
||||
<Pill>{$createOrganization.id}</Pill>
|
||||
{/if}
|
||||
</FormList>
|
||||
</div>
|
||||
|
||||
<p class="body-text-1 u-bold common-section">Plan</p>
|
||||
<p class="text">
|
||||
For more details on our plans, visit our <a
|
||||
class="link"
|
||||
href="http://#"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">pricing page</a
|
||||
>.
|
||||
</p>
|
||||
<ul
|
||||
class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-16"
|
||||
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
|
||||
{#if !anyOrgFree}
|
||||
<li>
|
||||
<LabelCard name="plan" bind:group={$createOrganization.tier} value="free">
|
||||
<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">Free - $0/month</h4>
|
||||
<p class="u-color-text-gray u-small">For personal, passion projects.</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
{/if}
|
||||
<li>
|
||||
<LabelCard name="plan" bind:group={$createOrganization.tier} value="starter">
|
||||
<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">
|
||||
Starter - $10/month per organization member
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">
|
||||
For small organizations that want flexibility.
|
||||
</p>
|
||||
</div>
|
||||
<Pill>14 DAY FREE TRIAL</Pill>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
<li>
|
||||
<LabelCard name="plan" bind:group={$createOrganization.tier} value="pro">
|
||||
<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">
|
||||
Pro - $20/month per organization member + exta usage
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">
|
||||
For organizations that need the ability scale easily.
|
||||
</p>
|
||||
</div>
|
||||
<Pill>14 DAY FREE TRIAL</Pill>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="u-margin-block-start-32">
|
||||
<p class="body-text-1 u-bold">Organization members</p>
|
||||
<p class="text">{$createOrganization?.collaborators?.length ?? 0} invited</p>
|
||||
</div>
|
||||
<div class="u-margin-block-start-32">
|
||||
<p class="body-text-1 u-bold">Payment</p>
|
||||
<p class="text">{$createOrganization.payment}</p>
|
||||
</div>
|
||||
|
||||
<div class="box u-margin-block-start-32">
|
||||
<p class="text">{$createOrganization.tier} plan</p>
|
||||
<p class="text">Invited members</p>
|
||||
<p class="text">Credit added</p>
|
||||
<div class="u-sep-block-start" />
|
||||
<p class="text">Estimated total</p>
|
||||
|
||||
<p class="text">
|
||||
This amount and any extra usage fees will be charged on a recurring 30 day billing cycle
|
||||
after your trial period ends on DATE
|
||||
</p>
|
||||
</div>
|
||||
</WizardStep>
|
||||
|
||||
Reference in New Issue
Block a user