Skip to content

Commit 28210cd

Browse files
committed
Merge branch 'main' into 'billing-sdk-refactor'.
2 parents 0e921a6 + 26bcf6f commit 28210cd

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<script lang="ts">
2+
import { Modal } from '$lib/components';
3+
import { Button, InputSelect } from '$lib/elements/forms';
4+
import { invalidate } from '$app/navigation';
5+
import { Dependencies } from '$lib/constants';
6+
import { addNotification } from '$lib/stores/notifications';
7+
import { sdk } from '$lib/stores/sdk';
8+
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
9+
import { states } from './state';
10+
import { Alert, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
11+
import { CreditCardBrandImage } from '../index.js';
12+
import type { Models } from '@appwrite.io/console';
13+
14+
let {
15+
show = $bindable(false),
16+
paymentMethod
17+
}: {
18+
show: boolean;
19+
paymentMethod: Models.PaymentMethod;
20+
} = $props();
21+
22+
let selectedState = $state('');
23+
let isSubmitting = $state(false);
24+
let error = $state<string | null>(null);
25+
26+
$effect(() => {
27+
if (!show) {
28+
selectedState = '';
29+
error = null;
30+
}
31+
});
32+
33+
async function handleSubmit() {
34+
if (!selectedState) {
35+
error = 'Please select a state';
36+
return;
37+
}
38+
39+
isSubmitting = true;
40+
error = null;
41+
42+
try {
43+
await sdk.forConsole.account.updatePaymentMethod({
44+
paymentMethodId: paymentMethod.$id,
45+
expiryMonth: paymentMethod.expiryMonth,
46+
expiryYear: paymentMethod.expiryYear,
47+
state: selectedState
48+
});
49+
trackEvent(Submit.PaymentMethodUpdate);
50+
await invalidate(Dependencies.PAYMENT_METHODS);
51+
addNotification({
52+
type: 'success',
53+
message: 'Payment method state has been updated'
54+
});
55+
show = false;
56+
} catch (e) {
57+
error = e.message;
58+
trackError(e, Submit.PaymentMethodUpdate);
59+
} finally {
60+
isSubmitting = false;
61+
}
62+
}
63+
</script>
64+
65+
<Modal
66+
bind:error
67+
bind:show
68+
dismissible={false}
69+
onSubmit={handleSubmit}
70+
title="Update payment method state">
71+
<Layout.Stack direction="column" gap="m">
72+
<Typography.Text>
73+
State information is required for US payment methods to apply correct taxes and meet
74+
U.S. legal requirements.
75+
</Typography.Text>
76+
{#if paymentMethod}
77+
<Card.Base variant="secondary" padding="s">
78+
<Layout.Stack direction="row" alignItems="center" gap="s">
79+
<CreditCardBrandImage brand={paymentMethod.brand} />
80+
<span>ending in {paymentMethod.last4}</span>
81+
</Layout.Stack>
82+
<Typography.Text size="s">
83+
{paymentMethod.country}
84+
</Typography.Text>
85+
</Card.Base>
86+
{/if}
87+
88+
<Alert.Inline status="info" title="State is required for US payment methods">
89+
<Typography.Text size="s">
90+
To complete the billing information, select your state so we can apply the correct
91+
taxes and meet U.S. legal requirements.
92+
</Typography.Text>
93+
</Alert.Inline>
94+
95+
<InputSelect
96+
bind:value={selectedState}
97+
required
98+
label="State"
99+
placeholder="Select a state"
100+
id="state-picker"
101+
options={states.map((stateOption) => ({
102+
label: stateOption.name,
103+
value: stateOption.abbreviation,
104+
id: stateOption.abbreviation.toLowerCase()
105+
}))} />
106+
</Layout.Stack>
107+
108+
<svelte:fragment slot="footer">
109+
<Button submit disabled={!selectedState || isSubmitting}>Save</Button>
110+
</svelte:fragment>
111+
</Modal>

src/routes/(console)/account/payments/paymentMethods.svelte

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import DeletePaymentModal from './deletePaymentModal.svelte';
99
import { hasStripePublicKey, isCloud } from '$lib/system';
1010
import PaymentModal from '$lib/components/billing/paymentModal.svelte';
11+
import UpdateStateModal from '$lib/components/billing/updateStateModal.svelte';
1112
import {
1213
IconDotsHorizontal,
1314
IconInfo,
@@ -33,6 +34,8 @@
3334
let selectedLinkedOrgs: Array<Models.Organization> = [];
3435
let showDelete = false;
3536
let showEdit = false;
37+
let showUpdateState = false;
38+
let paymentMethodNeedingState: Models.PaymentMethod | null = null;
3639
let isLinked = false;
3740
3841
$: orgList = $organizationList.teams as unknown as Array<Models.Organization>;
@@ -47,6 +50,27 @@
4750
);
4851
$: hasLinkedOrgs = filteredMethods.some((method) => linkedMethodIds.has(method.$id));
4952
$: hasPaymentError = filteredMethods.some((method) => method?.lastError || method?.expired);
53+
54+
// Check for US payment methods without state
55+
$: {
56+
if ($paymentMethods?.paymentMethods && !showUpdateState && !paymentMethodNeedingState) {
57+
const usMethodWithoutState = $paymentMethods.paymentMethods.find(
58+
(method: Models.PaymentMethod) =>
59+
method?.country?.toLowerCase() === 'us' &&
60+
(!method.state || method.state.trim() === '') &&
61+
!!method.last4
62+
);
63+
if (usMethodWithoutState) {
64+
paymentMethodNeedingState = usMethodWithoutState;
65+
showUpdateState = true;
66+
}
67+
}
68+
}
69+
70+
// Reset when modal is closed
71+
$: if (!showUpdateState && paymentMethodNeedingState) {
72+
paymentMethodNeedingState = null;
73+
}
5074
</script>
5175

5276
<CardGrid overflow={false}>
@@ -168,3 +192,6 @@
168192
bind:showDelete
169193
linkedOrgs={selectedLinkedOrgs} />
170194
{/if}
195+
{#if showUpdateState && paymentMethodNeedingState && isCloud && hasStripePublicKey}
196+
<UpdateStateModal bind:show={showUpdateState} paymentMethod={paymentMethodNeedingState} />
197+
{/if}

src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import ReplaceCard from './replaceCard.svelte';
1212
import EditPaymentModal from '$routes/(console)/account/payments/editPaymentModal.svelte';
1313
import PaymentModal from '$lib/components/billing/paymentModal.svelte';
14+
import UpdateStateModal from '$lib/components/billing/updateStateModal.svelte';
1415
import { user } from '$lib/stores/user';
1516
import {
1617
ActionMenu,
@@ -50,7 +51,9 @@
5051
let showDelete = $state(false);
5152
let showPayment = $state(false);
5253
let showReplace = $state(false);
54+
let showUpdateState = $state(false);
5355
let isSelectedBackup = $state(false);
56+
let paymentMethodNeedingState: Models.PaymentMethod | null = $state(null);
5457
5558
const hasPaymentError = $derived.by(() => {
5659
return (
@@ -107,6 +110,28 @@
107110
isSelectedBackup = false;
108111
}
109112
});
113+
114+
$effect(() => {
115+
if (paymentMethods?.paymentMethods && !showUpdateState && !paymentMethodNeedingState) {
116+
const usMethodWithoutState = paymentMethods.paymentMethods.find(
117+
(method: Models.PaymentMethod) =>
118+
method?.country?.toLowerCase() === 'us' &&
119+
(!method.state || method.state.trim() === '') &&
120+
!!method.last4
121+
);
122+
123+
if (usMethodWithoutState) {
124+
paymentMethodNeedingState = usMethodWithoutState;
125+
showUpdateState = true;
126+
}
127+
}
128+
});
129+
130+
$effect(() => {
131+
if (!showUpdateState && paymentMethodNeedingState) {
132+
paymentMethodNeedingState = null;
133+
}
134+
});
110135
</script>
111136

112137
<CardGrid overflow={false}>
@@ -339,3 +364,7 @@
339364
isBackup={isSelectedBackup}
340365
disabled={$currentPlan.requiresPaymentMethod && !hasOtherMethod} />
341366
{/if}
367+
368+
{#if showUpdateState && paymentMethodNeedingState && isCloud && hasStripePublicKey}
369+
<UpdateStateModal bind:show={showUpdateState} paymentMethod={paymentMethodNeedingState} />
370+
{/if}

0 commit comments

Comments
 (0)