Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/actions/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export enum Submit {
PaymentMethodCreate = 'submit_payment_method_create',
PaymentMethodUpdate = 'submit_payment_method_update',
PaymentMethodDelete = 'submit_payment_method_delete',
RetryPayment = 'submit_retry_payment',
BillingAddressCreate = 'submit_billing_address_create',
BillingAddressUpdate = 'submit_billing_address_update',
BillingAddressDelete = 'submit_billing_address_delete',
Expand Down
49 changes: 43 additions & 6 deletions src/lib/components/billing/paymentBoxes.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<script lang="ts">
import { FormList, InputText } from '$lib/elements/forms';
import { FormList, InputChoice, InputText } from '$lib/elements/forms';
import { onDestroy, onMount } from 'svelte';
import { CreditCardBrandImage, RadioBoxes } from '..';
import { unmountPaymentElement } from '$lib/stores/stripe';
import { Pill } from '$lib/elements';

export let methods: Record<string, unknown>[];
export let group: string;
export let name: string;
export let defaultMethod: string = null;
export let backupMethod: string = null;
export let disabledCondition: string = null;
export let setAsDefault = false;
export let showSetAsDefault = false;

let element: HTMLDivElement;
let loader: HTMLDivElement;
Expand Down Expand Up @@ -42,13 +48,36 @@
}
</script>

<RadioBoxes elements={methods} total={methods?.length} variableName="$id" name="payment" bind:group>
<RadioBoxes
elements={methods}
total={methods?.length}
variableName="$id"
name="payment"
bind:group
{disabledCondition}>
<svelte:fragment slot="element" let:element>
<slot {element}>
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
<span>
<span class="u-capitalize">{element.brand}</span> ending in {element.last4}</span>
<CreditCardBrandImage brand={element.brand?.toString()} />
<span class="u-flex u-gap-16 u-flex-vertical">
<span class="u-flex u-gap-16">
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
<span>
<span class="u-capitalize">{element.brand}</span> ending in {element.last4}</span>
<CreditCardBrandImage brand={element.brand?.toString()} />
</span>
{#if element.$id === backupMethod}
<Pill>Backup</Pill>
{:else if element.$id === defaultMethod}
<Pill>Default</Pill>
{/if}
</span>
{#if !!defaultMethod && element.$id !== defaultMethod && group === element.$id && showSetAsDefault && element.$id !== backupMethod}
<ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
</ul>
{/if}
</span>
</slot>
</svelte:fragment>
Expand All @@ -73,6 +102,14 @@
<!-- Stripe will create form elements here -->
</div>
</div>
{#if showSetAsDefault}
<ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
</ul>
{/if}
</FormList>
</RadioBoxes>

Expand Down
8 changes: 4 additions & 4 deletions src/lib/components/radioBoxes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{#if total}
{#each elements as element}
{@const value = element[variableName]?.toString()}
<div class="box" data-private>
<ul class="box" data-private>
<InputRadio
id={`${name}-${value}`}
{value}
Expand All @@ -22,11 +22,11 @@
disabled={disabledCondition ? value === disabledCondition : false}>
<slot name="element" {element} />
</InputRadio>
</div>
</ul>
{/each}
{/if}

<div class="box">
<ul class="box">
{#if total}
<InputRadio id="payment-method" value={null} {name} bind:group>
<slot name="new">
Expand All @@ -37,5 +37,5 @@
{#if group === null}
<slot />
{/if}
</div>
</ul>
</div>
22 changes: 22 additions & 0 deletions src/lib/sdk/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,28 @@ export class Billing {
);
}

async retryPayment(
organizationId: string,
invoiceId: string,
paymentMethodId: string
): Promise<Invoice> {
const path = `/organizations/${organizationId}/invoices/${invoiceId}/payments`;
const params = {
organizationId,
invoiceId,
paymentMethodId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
'post',
uri,
{
'content-type': 'application/json'
},
params
);
}

async listUsage(
organizationId: string,
startDate: string = undefined,
Expand Down
10 changes: 8 additions & 2 deletions src/lib/stores/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,15 @@ export async function submitStripeCard(name: string, urlRoute?: string) {
}
}

export async function confirmPayment(orgId: string, clientSecret: string, paymentMethodId: string) {
export async function confirmPayment(
orgId: string,
clientSecret: string,
paymentMethodId: string,
route?: string
) {
try {
const url = `${window.location.origin}/console/organization-${orgId}/billing`;
const url =
window.location.origin + (route ? route : `/console/organization-${orgId}/billing`);

const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentMethodId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
CardGrid,
DropList,
DropListItem,
DropListLink,
EmptySearch,
Heading,
Expand All @@ -20,13 +21,17 @@
TableScroll
} from '$lib/elements/table';
import { toLocaleDate } from '$lib/helpers/date';
import type { InvoiceList } from '$lib/sdk/billing';
import type { Invoice, InvoiceList } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { VARS } from '$lib/system';
import { Query } from '@appwrite.io/console';
import { onMount } from 'svelte';
import RetryPaymentModal from './retryPaymentModal.svelte';
import { trackEvent } from '$lib/actions/analytics';

let showDropdown = [];
let showRetryModal = false;
let selectedInvoice: Invoice | null = null;

let offset = 0;
let invoiceList: InvoiceList = {
Expand Down Expand Up @@ -122,6 +127,21 @@
event="download_invoice">
Download PDF
</DropListLink>
{#if status === 'overdue' || status === 'failed'}
<DropListItem
icon="refresh"
on:click={() => {
selectedInvoice = invoice;
showRetryModal = true;
showDropdown[i] = !showDropdown[i];
trackEvent(`click_retry_payment`, {
from: 'button',
source: 'billing_invoice_menu'
});
}}>
Retry payment
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
</TableCell>
Expand All @@ -144,3 +164,7 @@
{/if}
</svelte:fragment>
</CardGrid>

{#if selectedInvoice}
<RetryPaymentModal bind:show={showRetryModal} bind:invoice={selectedInvoice} />
{/if}
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { CreditCardBrandImage, FakeModal } from '$lib/components';
import { Button, FormList, InputRadio, InputText } from '$lib/elements/forms';
import { FakeModal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { organization } from '$lib/stores/organization';
import { Dependencies } from '$lib/constants';
import { initializeStripe, isStripeInitialized, submitStripeCard } from '$lib/stores/stripe';
import { onDestroy, onMount } from 'svelte';
import { submitStripeCard } from '$lib/stores/stripe';
import { onMount } from 'svelte';
import type { PaymentList } from '$lib/sdk/billing';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Pill } from '$lib/elements';
import { PaymentBoxes } from '$lib/components/billing';

export let show = false;
export let isBackup = false;
let methods: PaymentList;
let selectedPaymentMethodId: string;
let name: string;
let error: string;
let element: HTMLDivElement;
let loader: HTMLDivElement;

let observer: MutationObserver;

onMount(async () => {
methods = await sdk.forConsole.billing.listPaymentMethods();
Expand All @@ -32,27 +28,6 @@
selectedPaymentMethodId = isBackup
? $organization.backupPaymentMethodId
: $organization.paymentMethodId;

observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length > 0) {
for (let node of Array.from(mutation.addedNodes)) {
if (
node instanceof Element &&
node.className.toLowerCase().includes('__privatestripeelement')
) {
loader.style.display = 'none';
}
}
}
}
}
});
});

onDestroy(() => {
observer.disconnect();
});

async function handleSubmit() {
Expand Down Expand Up @@ -105,19 +80,8 @@
}
}

$: if (selectedPaymentMethodId === null && !$isStripeInitialized && show) {
initializeStripe();
}

$: filteredMethods = methods?.paymentMethods.filter((method) => !!method?.last4);

$: if (show) {
isStripeInitialized.set(false);
if (element) {
observer.observe(element, { childList: true });
}
}

$: if (!show) {
selectedPaymentMethodId = null;
}
Expand All @@ -131,70 +95,16 @@
headerDivider={false}
title="Replace payment method">
<p class="text">Replace the existing payment method for your organization.</p>
<FormList>
<div class:boxes-wrapper={methods?.total}>
{#if methods?.total}
{#each filteredMethods as method}
<div class="box">
<InputRadio
id={`payment-method-${method.$id}`}
disabled={isBackup
? method.$id === $organization.paymentMethodId
: method.$id === $organization.backupPaymentMethodId}
value={method.$id}
name="payment"
bind:group={selectedPaymentMethodId}>
<span class="u-flex u-gap-16 u-main-space-between">
<span
class="u-flex u-cross-center u-gap-8"
style="padding-inline:0.25rem">
<span>
<span class="u-capitalize">{method.brand}</span> ending in {method.last4}</span>
<CreditCardBrandImage brand={method.brand} />
</span>
{#if method.$id === $organization.backupPaymentMethodId}
<Pill>Backup</Pill>
{:else if method.$id === $organization.paymentMethodId}
<Pill>Default</Pill>
{/if}
</span>
</InputRadio>
</div>
{/each}
{/if}

<div class="box">
{#if methods?.total}
<InputRadio
id="payment-method"
value={null}
name="payment"
bind:group={selectedPaymentMethodId}>
<span style="padding-inline:0.25rem">Add new payment method</span>
</InputRadio>
{/if}
{#if selectedPaymentMethodId === null}
<FormList>
<InputText
id="name"
label="Cardholder name"
placeholder="Cardholder name"
bind:value={name}
required
autofocus={true} />
<div class="aw-stripe-container" data-private>
<div class="loader-container" bind:this={loader}>
<div class="loader"></div>
</div>
<div id="payment-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
</div>
</FormList>
{/if}
</div>
</div>
</FormList>
<PaymentBoxes
methods={filteredMethods}
bind:name
bind:group={selectedPaymentMethodId}
defaultMethod={$organization?.paymentMethodId}
backupMethod={$organization?.backupPaymentMethodId}
disabledCondition={isBackup
? $organization.paymentMethodId
: $organization.backupPaymentMethodId} />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button
Expand All @@ -206,17 +116,3 @@
</Button>
</svelte:fragment>
</FakeModal>

<style lang="scss">
.aw-stripe-container {
min-height: 295px;
position: relative;
.loader-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
}
}
</style>
Loading