feat: creditCardInfo component, create & change org fixes, small stuff

This commit is contained in:
Arman
2023-07-21 20:10:28 +02:00
parent 13b6c53bc8
commit 98418691f0
11 changed files with 224 additions and 179 deletions
+45 -35
View File
@@ -1,8 +1,10 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import type { PaymentMethodData } from '$lib/sdk/billing';
import { getCreditCardImage } from '$lib/stores/billing';
import { DropList, DropListItem } from '.';
export let isBox = false;
export let paymentMethod: PaymentMethodData;
export let isEditDisabled = false;
export let isDeleteDisabled = false;
@@ -10,47 +12,55 @@
let showDropdown = false;
</script>
<div class="box u-flex u-main-space-between u-cross-start">
<div>
<p class="text">
Card ending in {paymentMethod?.last4}
<span>{paymentMethod?.brand}</span>
</p>
<div class:box={isBox} class="u-flex u-main-space-between u-cross-start">
<div class="u-line-height-1-5">
<span class="u-flex u-cross-center u-gap-8">
<p class="text u-bold">
<span class="u-capitalize">{paymentMethod?.brand}</span> ending in {paymentMethod?.last4}
</p>
<img
width="23"
height="16"
src={getCreditCardImage(paymentMethod?.brand)}
alt={paymentMethod?.brand} />
</span>
<p class="text">
Expires {paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}
</p>
<p class="text">
<!-- <p class="text">
{paymentMethod?.name}
</p>
</p> -->
</div>
<DropList bind:show={showDropdown} placement="bottom-start" noArrow>
<Button
round
text
ariaLabel="More options"
on:click={() => {
showDropdown = !showDropdown;
}}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</Button>
<svelte:fragment slot="list">
<DropListItem
icon="pencil"
disabled={isEditDisabled}
<slot>
<DropList bind:show={showDropdown} placement="bottom-start" noArrow>
<Button
round
text
ariaLabel="More options"
on:click={() => {
console.log('test');
showDropdown = !showDropdown;
}}>
Edit
</DropListItem>
<DropListItem
icon="trash"
disabled={isDeleteDisabled}
on:click={() => {
console.log('test');
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
<span class="icon-dots-horizontal" aria-hidden="true" />
</Button>
<svelte:fragment slot="list">
<DropListItem
icon="pencil"
disabled={isEditDisabled}
on:click={() => {
console.log('test');
}}>
Edit
</DropListItem>
<DropListItem
icon="trash"
disabled={isDeleteDisabled}
on:click={() => {
console.log('test');
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</slot>
</div>
+6 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { FormItem, Helper, Label } from '.';
export let label: string;
export let label: string = null;
export let showLabel = true;
export let id: string;
export let group: string;
@@ -41,7 +41,11 @@
bind:this={element}
on:invalid={handleInvalid} />
<Label {required} hide={!showLabel} for={id}>
{label}
{#if label}
{label}
{:else}
<slot />
{/if}
</Label>
</div>
{#if error}
+6
View File
@@ -1,5 +1,6 @@
import { page } from '$app/stores';
import { derived } from 'svelte/store';
import { sdk } from './sdk';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2';
@@ -18,6 +19,11 @@ export function tierToPlan(tier: Tier) {
}
}
export function getCreditCardImage(brand: string, width = 46, height = 32) {
if (!brand) return '';
return sdk.forConsole.avatars.getCreditCard(brand, width, height).toString();
}
export const tierFree = {
price: 0,
name: 'Free',
+7 -6
View File
@@ -2,7 +2,7 @@ import { hasStripePublicKey, VARS } from '$lib/system';
import { loadStripe, type Stripe, type StripeCardElement } from '@stripe/stripe-js';
import { sdk } from './sdk';
import { app } from './app';
import { get } from 'svelte/store';
import { get, writable } from 'svelte/store';
import { apperanceDark, apperanceLight } from './billing';
import type { PaymentMethodData } from '$lib/sdk/billing';
@@ -11,10 +11,12 @@ let paymentMethod: PaymentMethodData;
let clientSecret: string;
let paymentElement: StripeCardElement;
export async function initializeStripe(isStripeInitialized: boolean) {
export const isStripeInitialized = writable(false);
export async function initializeStripe() {
if (!hasStripePublicKey) return;
stripe = await loadStripe(VARS.STRIPE_PUBLIC_KEY);
isStripeInitialized = true;
isStripeInitialized.set(true);
try {
const methods = await sdk.forConsole.billing.listPaymentMethods();
@@ -39,7 +41,6 @@ export async function initializeStripe(isStripeInitialized: boolean) {
const elements = stripe.elements(options);
paymentElement = elements.create('card');
paymentElement.mount('#payment-element');
return isStripeInitialized;
} catch (e) {
console.log(e);
}
@@ -64,12 +65,12 @@ export async function submitStripeCard(name: string) {
});
if (setupIntent && setupIntent.status === 'succeeded') {
await sdk.forConsole.billing.updatePaymentMethod(
const method = await sdk.forConsole.billing.updatePaymentMethod(
paymentMethod.$id,
setupIntent.payment_method
);
paymentElement.destroy();
return method;
} else console.log('something went wrong');
} catch (e) {
console.log(e);
@@ -1,5 +1,12 @@
<script lang="ts">
import { CardGrid, DropList, DropListItem, Empty, Heading } from '$lib/components';
import {
CardGrid,
CreditCardInfo,
DropList,
DropListItem,
Empty,
Heading
} from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
@@ -19,6 +26,8 @@
let showDelete = false;
console.log($paymentMethods);
$: filteredMethods = $paymentMethods?.paymentMethods.filter((method) => !!method?.last4);
</script>
<CardGrid>
@@ -33,65 +42,47 @@
<Table noMargin noStyles>
<TableHeader>
<TableCellHead>Payment methods</TableCellHead>
<TableCellHead width={220} />
<TableCellHead width={50} />
</TableHeader>
<TableBody>
{#each $paymentMethods.paymentMethods as paymentMethod, i}
{#each filteredMethods as paymentMethod, i}
<TableRow>
<TableCell>
<p class="text u-bold">
{paymentMethod.brand} ending in {paymentMethod.last4}
<span>{paymentMethod.brand}</span>
</p>
<p class="text">
Expires {paymentMethod.expiryMonth}/{paymentMethod.expiryYear}
</p>
</TableCell>
<TableCell>
{#if paymentMethod.default}
<Pill>
<span class="icon-info" /> default payment method
</Pill>
{/if}
{#if paymentMethod.backup}
<Pill>
<span class="icon-info" /> backup payment method
</Pill>
{/if}
</TableCell>
<TableCell showOverflow>
<DropList
bind:show={showDropdown[i]}
placement="bottom-start"
noArrow>
<Button
round
text
on:click={() => {
showDropdown[i] = !showDropdown[i];
}}>
<span class="icon-dots-horizontal" />
</Button>
<svelte:fragment slot="list">
<DropListItem
icon="pencil"
on:click={() => {
console.log('test');
}}>
Edit
</DropListItem>
<DropListItem
icon="trash"
on:click={() => {
selectedMethod = paymentMethod.$id;
showDelete = true;
showDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
<CreditCardInfo {paymentMethod}>
<div class="u-flex u-gap-16">
<Pill>test</Pill>
<DropList
bind:show={showDropdown[i]}
placement="bottom-start"
noArrow>
<Button
round
text
on:click={() => {
showDropdown[i] = !showDropdown[i];
}}>
<span class="icon-dots-horizontal" />
</Button>
<svelte:fragment slot="list">
<DropListItem
icon="pencil"
on:click={() => {
console.log('test');
}}>
Edit
</DropListItem>
<DropListItem
icon="trash"
on:click={() => {
selectedMethod = paymentMethod.$id;
showDelete = true;
showDropdown[i] = false;
}}>
Delete
</DropListItem>
</svelte:fragment>
</DropList>
</div>
</CreditCardInfo>
</TableCell>
</TableRow>
{/each}
@@ -12,10 +12,8 @@
let name: string;
let error: string;
let isStripeInitialized = false;
onMount(async () => {
await initializeStripe(isStripeInitialized);
await initializeStripe();
});
async function handleSubmit() {
@@ -9,7 +9,7 @@
import { Button } from '$lib/elements/forms';
import PaymentModal from '$routes/console/account/payments/paymentModal.svelte';
import { hasStripePublicKey, isCloud } from '$lib/system';
import { paymentMethods } from '$lib/stores/billing';
import { getCreditCardImage, paymentMethods } from '$lib/stores/billing';
import { onMount } from 'svelte';
import type { PaymentMethodData } from '$lib/sdk/billing';
@@ -127,6 +127,7 @@
<h4 class="u-bold body-text-2">Default</h4>
{#if $organization?.paymentMethodId}
<CreditCardInfo
isBox
paymentMethod={defaultPaymentMethod}
isDeleteDisabled={!$organization?.backupPaymentMethodId} />
{:else}
@@ -139,13 +140,19 @@
</Button>
<svelte:fragment slot="list">
{#if $paymentMethods.total}
{#each $paymentMethods.paymentMethods.filter((o) => o.$id !== $organization.backupPaymentMethodId) as paymentMethod}
{#each $paymentMethods.paymentMethods.filter((o) => !!o.last4 && o.$id !== $organization.backupPaymentMethodId) as paymentMethod}
<DropListItem
on:click={() => addPaymentMethod(paymentMethod?.$id)}>
<p class="text">
Card ending in {paymentMethod.last4}
<span>{paymentMethod.brand}</span>
</p>
<span class="u-flex u-cross-center u-gap-8">
<p class="text">
Card ending in {paymentMethod.last4}
</p>
<img
width="23"
height="16"
src={getCreditCardImage(paymentMethod?.brand)}
alt={paymentMethod?.brand} />
</span>
</DropListItem>
{/each}
{/if}
@@ -176,14 +183,20 @@
</Button>
<svelte:fragment slot="list">
{#if $paymentMethods.total}
{#each $paymentMethods.paymentMethods.filter((o) => o.$id !== $organization?.paymentMethodId) as paymentMethod}
{#each $paymentMethods.paymentMethods.filter((o) => !!o.last4 && o.$id !== $organization?.paymentMethodId) as paymentMethod}
<DropListItem
on:click={() =>
addBackupPaymentMethod(paymentMethod?.$id)}>
<p class="text">
Card ending in {paymentMethod.last4}
<span>{paymentMethod.brand}</span>
</p>
<span class="u-flex u-cross-center u-gap-8">
<p class="text">
Card ending in {paymentMethod.last4}
</p>
<img
width="23"
height="16"
src={getCreditCardImage(paymentMethod?.brand)}
alt={paymentMethod?.brand} />
</span>
</DropListItem>
{/each}
{/if}
@@ -9,38 +9,42 @@
import { organization } from '$lib/stores/organization';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { initializeStripe, submitStripeCard } from '$lib/stores/stripe';
import { initializeStripe, isStripeInitialized, submitStripeCard } from '$lib/stores/stripe';
import { sdk } from '$lib/stores/sdk';
import { getCreditCardImage } from '$lib/stores/billing';
let methods: PaymentList;
let name: string;
let budgetEnabled = false;
let showRates = false;
let isStripeInitialized = false;
onMount(async () => {
methods = await sdk.forConsole.billing.listPaymentMethods();
$createOrganization.paymentMethodId =
methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null;
isStripeInitialized = await initializeStripe(isStripeInitialized);
// await initializeStripe();
});
async function handleSubmit() {
try {
await submitStripeCard(name);
const method = await submitStripeCard(name);
$createOrganization.paymentMethodId = method.$id;
invalidate(Dependencies.PAYMENT_METHODS);
} catch (e) {
console.log(e.message);
}
}
$: if ($createOrganization.paymentMethodId === null && !isStripeInitialized) {
initializeStripe(isStripeInitialized);
$: if ($createOrganization.paymentMethodId === null && !$isStripeInitialized) {
initializeStripe();
}
$: if ($createOrganization.paymentMethodId) {
isStripeInitialized = false;
isStripeInitialized.set(false);
}
$: filteredMethods = methods?.paymentMethods.filter((method) => !!method?.last4);
</script>
<WizardStep beforeSubmit={handleSubmit}>
@@ -50,14 +54,23 @@
<FormList>
<div class:boxes-wrapper={methods?.total}>
{#if methods?.total}
{#each methods.paymentMethods as method}
{#each filteredMethods as method}
<div class="box">
<InputRadio
id={`payment-method-${method.last4}`}
label={`${method.brand} ending in ${method.last4}`}
value={method.$id}
name="payment"
bind:group={$createOrganization.paymentMethodId} />
bind:group={$createOrganization.paymentMethodId}>
<span class="u-flex u-cross-center u-gap-8">
<span>
<span class="u-capitalize">{method.brand}</span> ending in {method.last4}</span>
<img
width="23"
height="16"
src={getCreditCardImage(method.brand)}
alt={method.brand} />
</span>
</InputRadio>
</div>
{/each}
{/if}
@@ -18,6 +18,7 @@
const card = await sdk.forConsole.billing.getPaymentMethod(
$createOrganization.paymentMethodId
);
console.log(card);
return card;
} catch (error) {
console.log(error);
@@ -39,33 +40,36 @@
{/if}
</div>
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Organization members</p>
<p class="text">{collaboratorsNumber} invited</p>
</div>
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Payment</p>
{#await fetchCard()}
<div class="u-flex u-margin-block-start-4">
<div class="loader is-small" />
</div>
{:then card}
<p class="text">Card ending in {card.last4} {card.brand}</p>
{/await}
</div>
{#if $createOrganization.billingPlan !== 'tier-0'}
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Organization members</p>
<p class="text">{collaboratorsNumber} invited</p>
</div>
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Payment</p>
{#await fetchCard()}
<div class="u-flex u-margin-block-start-4">
<div class="loader is-small" />
</div>
{:then card}
<p class="text">Card ending in {card.last4} {card.brand}</p>
{/await}
</div>
{/if}
<div class="box u-margin-block-start-32 u-flex u-flex-vertical u-gap-16">
<span class="u-flex u-main-space-between">
<p class="text">{plan.name} plan</p>
<p class="text">${plan.price}</p>
</span>
<span class="u-flex u-main-space-between">
<p class="text">Invited members</p>
<p class="text">${plan.collaboratorPrice * collaboratorsNumber}</p>
</span>
{#if $createOrganization.billingPlan !== 'tier-0'}
<span class="u-flex u-main-space-between">
<p class="text">{plan.name} plan</p>
<p class="text">${plan.price}</p>
</span>
<span class="u-flex u-main-space-between">
<p class="text">Invited members</p>
<p class="text">${plan.collaboratorPrice * collaboratorsNumber}</p>
</span>
{/if}
<div class="u-sep-block-start" />
<span class="u-flex u-main-space-between">
<p class="text">Estimated total</p>
<p class="text">Estimated total (in USD)</p>
<p class="text">${totalExpences}</p>
</span>
@@ -9,7 +9,8 @@
import type { PaymentList } from '$lib/sdk/billing';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { initializeStripe, submitStripeCard } from '$lib/stores/stripe';
import { initializeStripe, isStripeInitialized, submitStripeCard } from '$lib/stores/stripe';
import { organization } from '$lib/stores/organization';
let methods: PaymentList;
let name: string;
@@ -18,12 +19,10 @@
// let error: string;
let isStripeInitialized = false;
onMount(async () => {
methods = await sdk.forConsole.billing.listPaymentMethods();
isStripeInitialized = await initializeStripe(isStripeInitialized);
$changeOrganizationTier.paymentMethodId =
$organization?.backupPaymentMethodId ?? methods.paymentMethods[0]?.$id ?? null;
});
async function handleSubmit() {
@@ -35,13 +34,15 @@
}
}
$: if ($changeOrganizationTier.paymentMethodId === null && !isStripeInitialized) {
initializeStripe(isStripeInitialized);
$: if ($changeOrganizationTier.paymentMethodId === null && !$isStripeInitialized) {
initializeStripe();
}
$: if ($changeOrganizationTier.paymentMethodId) {
isStripeInitialized = false;
isStripeInitialized.set(false);
}
$: filteredMethods = methods?.paymentMethods.filter((method) => !!method?.last4);
</script>
<WizardStep beforeSubmit={handleSubmit}>
@@ -51,7 +52,7 @@
<FormList>
<div class:boxes-wrapper={methods?.total}>
{#if methods?.total}
{#each methods.paymentMethods as method}
{#each filteredMethods as method}
<div class="box">
<InputRadio
id={`payment-method-${method.last4}`}
@@ -40,30 +40,34 @@
{/if}
</div>
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Organization members</p>
<p class="text">{collaboratorsNumber} invited</p>
</div>
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Payment</p>
{#await fetchCard()}
<div class="u-flex u-margin-block-start-4">
<div class="loader is-small" />
</div>
{:then card}
<p class="text">Card ending in {card.last4} {card.brand}</p>
{/await}
</div>
{#if $changeOrganizationTier.billingPlan !== 'tier-0'}
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Organization members</p>
<p class="text">{collaboratorsNumber} invited</p>
</div>
<div class="u-margin-block-start-32">
<p class="body-text-1 u-bold">Payment</p>
{#await fetchCard()}
<div class="u-flex u-margin-block-start-4">
<div class="loader is-small" />
</div>
{:then card}
<p class="text">Card ending in {card.last4} {card.brand}</p>
{/await}
</div>
{/if}
<div class="box u-margin-block-start-32 u-flex u-flex-vertical u-gap-16">
<span class="u-flex u-main-space-between">
<p class="text">{plan.name} plan</p>
<p class="text">${plan.price}</p>
</span>
<span class="u-flex u-main-space-between">
<p class="text">Invited members</p>
<p class="text">${plan.collaboratorPrice * collaboratorsNumber}</p>
</span>
{#if $changeOrganizationTier.billingPlan !== 'tier-0'}
<span class="u-flex u-main-space-between">
<p class="text">{plan.name} plan</p>
<p class="text">${plan.price}</p>
</span>
<span class="u-flex u-main-space-between">
<p class="text">Invited members</p>
<p class="text">${plan.collaboratorPrice * collaboratorsNumber}</p>
</span>
{/if}
<div class="u-sep-block-start" />
<span class="u-flex u-main-space-between">
<p class="text">Estimated total</p>