Skip to content
Open
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
112 changes: 112 additions & 0 deletions src/lib/components/billing/updateStateModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, InputSelect } from '$lib/elements/forms';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import type { PaymentMethodData } from '$lib/sdk/billing';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { states } from './state';
import { Alert, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
import { CreditCardBrandImage } from '../index.js';

let {
show = $bindable(false),
paymentMethod
}: {
show: boolean;
paymentMethod: PaymentMethodData;
} = $props();

let selectedState = $state('');
let isSubmitting = $state(false);
let error = $state<string | null>(null);

$effect(() => {
if (!show) {
selectedState = '';
error = null;
}
});

async function handleSubmit() {
if (!selectedState) {
error = 'Please select a state';
return;
}

isSubmitting = true;
error = null;

try {
await sdk.forConsole.billing.setPaymentMethod(
paymentMethod.$id,
paymentMethod.providerMethodId,
paymentMethod.name,
selectedState
);
trackEvent(Submit.PaymentMethodUpdate);
await invalidate(Dependencies.PAYMENT_METHODS);
addNotification({
type: 'success',
message: 'Payment method state has been updated'
});
show = false;
} catch (e) {
error = e.message;
trackError(e, Submit.PaymentMethodUpdate);
} finally {
isSubmitting = false;
}
}
</script>

<Modal
dismissible={false}
bind:error
onSubmit={handleSubmit}
bind:show
title="Update payment method state">
<svelte:fragment slot="description">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move the desc below the separator
its too close to the title

State information is required for US payment methods to apply correct taxes and meet U.S.
legal requirements.
</svelte:fragment>

<Layout.Stack direction="column" gap="m">
{#if paymentMethod}
<Card.Base variant="secondary" padding="s">
<Layout.Stack direction="row" alignItems="center" gap="s">
<CreditCardBrandImage brand={paymentMethod.brand} />
<span>ending in {paymentMethod.last4}</span>
</Layout.Stack>
<Typography.Text size="s">
{paymentMethod.country}
</Typography.Text>
</Card.Base>
{/if}

<Alert.Inline status="info" title="State is required for US payment methods">
<Typography.Text size="s">
To complete the billing information, select your state so we can apply the correct
taxes and meet U.S. legal requirements.
</Typography.Text>
</Alert.Inline>

<InputSelect
bind:value={selectedState}
required
label="State"
placeholder="Select a state"
id="state-picker"
options={states.map((stateOption) => ({
label: stateOption.name,
value: stateOption.abbreviation,
id: stateOption.abbreviation.toLowerCase()
}))} />
</Layout.Stack>

<svelte:fragment slot="footer">
<Button submit disabled={!selectedState || isSubmitting}>Save</Button>
</svelte:fragment>
</Modal>
1 change: 1 addition & 0 deletions src/lib/sdk/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type PaymentMethodData = {
name: string;
mandateId?: string;
lastError?: string;
state?: string;
};

export type PaymentList = {
Expand Down
27 changes: 27 additions & 0 deletions src/routes/(console)/account/payments/paymentMethods.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import DeletePaymentModal from './deletePaymentModal.svelte';
import { hasStripePublicKey, isCloud } from '$lib/system';
import PaymentModal from '$lib/components/billing/paymentModal.svelte';
import UpdateStateModal from '$lib/components/billing/updateStateModal.svelte';
import {
IconDotsHorizontal,
IconInfo,
Expand All @@ -33,6 +34,8 @@
let selectedLinkedOrgs: Organization[] = [];
let showDelete = false;
let showEdit = false;
let showUpdateState = false;
let paymentMethodNeedingState: PaymentMethodData | null = null;
let isLinked = false;

$: orgList = $organizationList.teams as unknown as Organization[];
Expand All @@ -49,6 +52,27 @@
);
$: hasLinkedOrgs = filteredMethods.some((method) => linkedMethodIds.has(method.$id));
$: hasPaymentError = filteredMethods.some((method) => method?.lastError || method?.expired);

// Check for US payment methods without state
$: {
if ($paymentMethods?.paymentMethods && !showUpdateState && !paymentMethodNeedingState) {
const usMethodWithoutState = $paymentMethods.paymentMethods.find(
(method: PaymentMethodData) =>
method?.country?.toLowerCase() === 'us' &&
(!method.state || method.state.trim() === '') &&
!!method.last4
);
if (usMethodWithoutState) {
paymentMethodNeedingState = usMethodWithoutState;
showUpdateState = true;
}
}
}

// Reset when modal is closed
$: if (!showUpdateState && paymentMethodNeedingState) {
paymentMethodNeedingState = null;
}
</script>

<CardGrid overflow={false}>
Expand Down Expand Up @@ -170,3 +194,6 @@
bind:showDelete
linkedOrgs={selectedLinkedOrgs} />
{/if}
{#if showUpdateState && paymentMethodNeedingState && isCloud && hasStripePublicKey}
<UpdateStateModal bind:show={showUpdateState} paymentMethod={paymentMethodNeedingState} />
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import ReplaceCard from './replaceCard.svelte';
import EditPaymentModal from '$routes/(console)/account/payments/editPaymentModal.svelte';
import PaymentModal from '$lib/components/billing/paymentModal.svelte';
import UpdateStateModal from '$lib/components/billing/updateStateModal.svelte';
import { user } from '$lib/stores/user';
import {
ActionMenu,
Expand Down Expand Up @@ -44,6 +45,8 @@
let showEdit = false;
let showDelete = false;
let showReplace = false;
let showUpdateState = false;
let paymentMethodNeedingState: PaymentMethodData | null = null;
let isSelectedBackup = false;

async function addPaymentMethod(paymentMethodId: string) {
Expand Down Expand Up @@ -96,6 +99,27 @@
primaryMethod?.expired ||
backupMethod?.lastError ||
backupMethod?.expired;

// Check for US payment methods without state
$: {
if (methods?.paymentMethods && !showUpdateState && !paymentMethodNeedingState) {
const usMethodWithoutState = methods.paymentMethods.find(
(method: PaymentMethodData) =>
method?.country?.toLowerCase() === 'us' &&
(!method.state || method.state.trim() === '') &&
!!method.last4
);
if (usMethodWithoutState) {
paymentMethodNeedingState = usMethodWithoutState;
showUpdateState = true;
}
}
}

// Reset when modal is closed
$: if (!showUpdateState && paymentMethodNeedingState) {
paymentMethodNeedingState = null;
}
</script>

<CardGrid overflow={false}>
Expand Down Expand Up @@ -323,3 +347,6 @@
isBackup={isSelectedBackup}
disabled={organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod} />
{/if}
{#if showUpdateState && paymentMethodNeedingState && isCloud && hasStripePublicKey}
<UpdateStateModal bind:show={showUpdateState} paymentMethod={paymentMethodNeedingState} />
{/if}