feat: org creation, payment page WIP

This commit is contained in:
Arman
2023-06-21 18:02:24 +02:00
parent 2fc1a257e8
commit 12668e50c6
14 changed files with 378 additions and 124 deletions
+6
View File
@@ -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} />
-1
View File
@@ -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"
+51 -40
View File
@@ -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>
+31 -83
View File
@@ -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>