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
111 changes: 111 additions & 0 deletions src/lib/components/billing/updateStateModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<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';
Comment on lines +10 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use $lib alias for src/lib imports.

Relative imports from src/lib should use the $lib alias. This keeps paths consistent and matches the repo conventions.

🔧 Suggested change
-import { states } from './state';
+import { states } from '$lib/components/billing/state';
@@
-import { CreditCardBrandImage } from '../index.js';
+import { CreditCardBrandImage } from '$lib/components';

As per coding guidelines, ...

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { states } from './state';
import { Alert, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
import { CreditCardBrandImage } from '../index.js';
import { states } from '$lib/components/billing/state';
import { Alert, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
import { CreditCardBrandImage } from '$lib/components';
🤖 Prompt for AI Agents
In `@src/lib/components/billing/updateStateModal.svelte` around lines 10 - 12,
Update the imports in updateStateModal.svelte to use the $lib alias instead of
relative paths: replace the import of states (currently from './state') and
CreditCardBrandImage (currently from '../index.js') to import from '$lib/...'
equivalents so they resolve from src/lib; ensure the module names (states and
CreditCardBrandImage) remain unchanged and update any other src/lib relative
imports in this file to use the $lib alias for consistency with repo
conventions.


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">
<Layout.Stack direction="column" gap="m">
<Typography.Text>
State information is required for US payment methods to apply correct taxes and meet
U.S. legal requirements.
</Typography.Text>
{#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}
Loading