diff --git a/e2e/steps/pro-project.ts b/e2e/steps/pro-project.ts index 0762132c4c..438caebef9 100644 --- a/e2e/steps/pro-project.ts +++ b/e2e/steps/pro-project.ts @@ -20,6 +20,9 @@ export async function enterCreditCard(page: Page) { await stripe.locator('id=Field-cvcInput').fill('123'); await stripe.locator('id=Field-countryInput').selectOption('DE'); await dialog.getByRole('button', { name: 'Add', exact: true }).click(); + await page.locator('id=state-picker').click(); // open dropdown + await page.getByRole('option', { name: 'Alabama' }).click(); + await dialog.getByRole('button', { name: 'Add', exact: true }).click(); await dialog.waitFor({ state: 'hidden' }); diff --git a/src/lib/components/billing/paymentBoxes.svelte b/src/lib/components/billing/paymentBoxes.svelte index be241fdf7f..27719cba54 100644 --- a/src/lib/components/billing/paymentBoxes.svelte +++ b/src/lib/components/billing/paymentBoxes.svelte @@ -5,6 +5,8 @@ import { initializeStripe, unmountPaymentElement } from '$lib/stores/stripe'; import { Badge, Card, Layout } from '@appwrite.io/pink-svelte'; import type { PaymentMethodData } from '$lib/sdk/billing'; + import type { PaymentMethod } from '@stripe/stripe-js'; + import StatePicker from './statePicker.svelte'; export let methods: PaymentMethodData[]; export let group: string; @@ -14,6 +16,9 @@ export let disabledCondition: string = null; export let setAsDefault = false; export let showSetAsDefault = false; + export let showState = false; + export let paymentMethod: PaymentMethod | null = null; + export let state: string = ''; let element: HTMLDivElement; let loader: HTMLDivElement; @@ -79,25 +84,29 @@ {/each} {#if group === '$new'} - + {#if showState} + + {:else} + -
-
-
+
+
+
+
+
-
-
- {#if showSetAsDefault} - + {#if showSetAsDefault} + + {/if} {/if} {/if} diff --git a/src/lib/components/billing/paymentModal.svelte b/src/lib/components/billing/paymentModal.svelte index c79602517d..72accfd101 100644 --- a/src/lib/components/billing/paymentModal.svelte +++ b/src/lib/components/billing/paymentModal.svelte @@ -2,12 +2,14 @@ import { FakeModal } from '$lib/components'; import { InputText, Button } from '$lib/elements/forms'; import { createEventDispatcher, onMount } from 'svelte'; - import { initializeStripe, submitStripeCard } from '$lib/stores/stripe'; + import { initializeStripe, setPaymentMethod, submitStripeCard } from '$lib/stores/stripe'; import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { addNotification } from '$lib/stores/notifications'; import { page } from '$app/state'; import { Spinner } from '@appwrite.io/pink-svelte'; + import type { PaymentMethod } from '@stripe/stripe-js'; + import StatePicker from './statePicker.svelte'; export let show = false; @@ -16,10 +18,36 @@ let name: string; let error: string; let modal: FakeModal; + let showState: boolean = false; + let state: string = ''; + let paymentMethod: PaymentMethod | null = null; async function handleSubmit() { try { + if (showState && !state) { + throw Error('Please select a state'); + } + + if (showState) { + const card = await setPaymentMethod(paymentMethod.id, name, state); + modal.closeModal(); + await invalidate(Dependencies.PAYMENT_METHODS); + dispatch('submit', card); + addNotification({ + type: 'success', + message: 'A new payment method has been added to your account' + }); + return; + } + const card = await submitStripeCard(name, page?.params?.organization ?? null); + if (card && Object.hasOwn(card, 'id')) { + if ((card as PaymentMethod).card.country === 'US') { + paymentMethod = card as PaymentMethod; + showState = true; + return; + } + } modal.closeModal(); await invalidate(Dependencies.PAYMENT_METHODS); dispatch('submit', card); @@ -73,28 +101,30 @@ bind:error onSubmit={handleSubmit}> - - -
- {#if isLoading} -
- + {#if showState} + + {:else} + +
+ {#if isLoading} +
+ +
+ {/if} +
+
- {/if} - -
-
-
+ {/if} diff --git a/src/lib/components/billing/state.ts b/src/lib/components/billing/state.ts new file mode 100644 index 0000000000..d3f08a1136 --- /dev/null +++ b/src/lib/components/billing/state.ts @@ -0,0 +1,238 @@ +export const states: Array<{ name: string; abbreviation: string }> = [ + { + name: 'Alabama', + abbreviation: 'AL' + }, + { + name: 'Alaska', + abbreviation: 'AK' + }, + { + name: 'American Samoa', + abbreviation: 'AS' + }, + { + name: 'Arizona', + abbreviation: 'AZ' + }, + { + name: 'Arkansas', + abbreviation: 'AR' + }, + { + name: 'California', + abbreviation: 'CA' + }, + { + name: 'Colorado', + abbreviation: 'CO' + }, + { + name: 'Connecticut', + abbreviation: 'CT' + }, + { + name: 'Delaware', + abbreviation: 'DE' + }, + { + name: 'District Of Columbia', + abbreviation: 'DC' + }, + { + name: 'Federated States Of Micronesia', + abbreviation: 'FM' + }, + { + name: 'Florida', + abbreviation: 'FL' + }, + { + name: 'Georgia', + abbreviation: 'GA' + }, + { + name: 'Guam', + abbreviation: 'GU' + }, + { + name: 'Hawaii', + abbreviation: 'HI' + }, + { + name: 'Idaho', + abbreviation: 'ID' + }, + { + name: 'Illinois', + abbreviation: 'IL' + }, + { + name: 'Indiana', + abbreviation: 'IN' + }, + { + name: 'Iowa', + abbreviation: 'IA' + }, + { + name: 'Kansas', + abbreviation: 'KS' + }, + { + name: 'Kentucky', + abbreviation: 'KY' + }, + { + name: 'Louisiana', + abbreviation: 'LA' + }, + { + name: 'Maine', + abbreviation: 'ME' + }, + { + name: 'Marshall Islands', + abbreviation: 'MH' + }, + { + name: 'Maryland', + abbreviation: 'MD' + }, + { + name: 'Massachusetts', + abbreviation: 'MA' + }, + { + name: 'Michigan', + abbreviation: 'MI' + }, + { + name: 'Minnesota', + abbreviation: 'MN' + }, + { + name: 'Mississippi', + abbreviation: 'MS' + }, + { + name: 'Missouri', + abbreviation: 'MO' + }, + { + name: 'Montana', + abbreviation: 'MT' + }, + { + name: 'Nebraska', + abbreviation: 'NE' + }, + { + name: 'Nevada', + abbreviation: 'NV' + }, + { + name: 'New Hampshire', + abbreviation: 'NH' + }, + { + name: 'New Jersey', + abbreviation: 'NJ' + }, + { + name: 'New Mexico', + abbreviation: 'NM' + }, + { + name: 'New York', + abbreviation: 'NY' + }, + { + name: 'North Carolina', + abbreviation: 'NC' + }, + { + name: 'North Dakota', + abbreviation: 'ND' + }, + { + name: 'Northern Mariana Islands', + abbreviation: 'MP' + }, + { + name: 'Ohio', + abbreviation: 'OH' + }, + { + name: 'Oklahoma', + abbreviation: 'OK' + }, + { + name: 'Oregon', + abbreviation: 'OR' + }, + { + name: 'Palau', + abbreviation: 'PW' + }, + { + name: 'Pennsylvania', + abbreviation: 'PA' + }, + { + name: 'Puerto Rico', + abbreviation: 'PR' + }, + { + name: 'Rhode Island', + abbreviation: 'RI' + }, + { + name: 'South Carolina', + abbreviation: 'SC' + }, + { + name: 'South Dakota', + abbreviation: 'SD' + }, + { + name: 'Tennessee', + abbreviation: 'TN' + }, + { + name: 'Texas', + abbreviation: 'TX' + }, + { + name: 'Utah', + abbreviation: 'UT' + }, + { + name: 'Vermont', + abbreviation: 'VT' + }, + { + name: 'Virgin Islands', + abbreviation: 'VI' + }, + { + name: 'Virginia', + abbreviation: 'VA' + }, + { + name: 'Washington', + abbreviation: 'WA' + }, + { + name: 'West Virginia', + abbreviation: 'WV' + }, + { + name: 'Wisconsin', + abbreviation: 'WI' + }, + { + name: 'Wyoming', + abbreviation: 'WY' + } +]; diff --git a/src/lib/components/billing/statePicker.svelte b/src/lib/components/billing/statePicker.svelte new file mode 100644 index 0000000000..839b31dd91 --- /dev/null +++ b/src/lib/components/billing/statePicker.svelte @@ -0,0 +1,44 @@ + + + + {#if card} + + + + ending in {card.card?.last4} + + + {card.card.country} + {card.billing_details.address.postal_code} + + + {/if} + + + + To complete the billing information, select your state so we can apply the correct taxes + and meet U.S. legal requirements. + + + + ({ + label: state.name, + value: state.abbreviation, + id: state.abbreviation.toLowerCase() + }))} /> + diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index b1d3b58096..3dbd0b9f9d 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -1111,7 +1111,8 @@ export class Billing { async setPaymentMethod( paymentMethodId: string, providerMethodId: string | PaymentMethod, - name: string + name: string, + state: string | undefined = undefined ): Promise { const path = `/account/payment-methods/${paymentMethodId}/provider`; const params = { @@ -1119,6 +1120,10 @@ export class Billing { providerMethodId, name }; + + if (state !== undefined) { + params['state'] = state; + } const uri = new URL(this.client.config.endpoint + path); return await this.client.call( 'patch', diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index 0f71da6425..7e5fc31d6b 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -1,4 +1,10 @@ -import type { Appearance, Stripe, StripeElement, StripeElements } from '@stripe/stripe-js'; +import type { + Appearance, + PaymentMethod, + Stripe, + StripeElement, + StripeElements +} from '@stripe/stripe-js'; import { sdk } from './sdk'; import { app } from './app'; import { get, writable } from 'svelte/store'; @@ -83,7 +89,8 @@ export async function submitStripeCard(name: string, organizationId?: string) { billing_details: { name } - } + }, + expand: ['payment_method'] }, redirect: 'if_required' }); @@ -95,9 +102,13 @@ export async function submitStripeCard(name: string, organizationId?: string) { } if (setupIntent && setupIntent.status === 'succeeded') { + if ((setupIntent.payment_method as PaymentMethod).card.country === 'US') { + // need to get state + return setupIntent.payment_method as PaymentMethod; + } const method = await sdk.forConsole.billing.setPaymentMethod( paymentMethod.$id, - setupIntent.payment_method, + (setupIntent.payment_method as PaymentMethod).id, name ); paymentElement.destroy(); @@ -115,6 +126,32 @@ export async function submitStripeCard(name: string, organizationId?: string) { } } +export async function setPaymentMethod(providerMethodId: string, name: string, state: string) { + if (!paymentMethod) { + addNotification({ + title: 'Error', + message: 'No payment method found. Please try again.', + type: 'error' + }); + return; + } + try { + const method = await sdk.forConsole.billing.setPaymentMethod( + paymentMethod.$id, + providerMethodId, + name, + state + ); + paymentElement.destroy(); + isStripeInitialized.set(false); + trackEvent(Submit.PaymentMethodCreate); + return method; + } catch (e) { + trackError(e, Submit.PaymentMethodCreate); + throw e; + } +} + export async function confirmPayment( orgId: string, clientSecret: string, diff --git a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte index 9adbb514ed..a0b17f7746 100644 --- a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte +++ b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte @@ -5,12 +5,13 @@ import { sdk } from '$lib/stores/sdk'; import type { Organization } from '$lib/stores/organization'; import { Dependencies } from '$lib/constants'; - import { submitStripeCard } from '$lib/stores/stripe'; + import { setPaymentMethod, submitStripeCard } from '$lib/stores/stripe'; import { onMount } from 'svelte'; - import type { PaymentList } from '$lib/sdk/billing'; + import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing'; import { addNotification } from '$lib/stores/notifications'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { PaymentBoxes } from '$lib/components/billing'; + import type { PaymentMethod } from '@stripe/stripe-js'; export let organization: Organization; export let show = false; @@ -20,6 +21,9 @@ let name: string; let error: string; let selectedPaymentMethodId: string; + let showState: boolean = false; + let state: string = ''; + let paymentMethod: PaymentMethod | null = null; onMount(async () => { if (!organization.paymentMethodId && !organization.backupPaymentMethodId) { @@ -41,7 +45,24 @@ async function handleSubmit() { try { if (selectedPaymentMethodId === '$new') { - const method = await submitStripeCard(name, organization.$id); + if (showState && !state) { + throw Error('Please select a state'); + } + let method: PaymentMethodData; + if (showState) { + method = await setPaymentMethod(paymentMethod.id, name, state); + } else { + const card = await submitStripeCard(name, organization.$id); + if (card && Object.hasOwn(card, 'id')) { + if ((card as PaymentMethod).card.country === 'US') { + paymentMethod = card as PaymentMethod; + showState = true; + return; + } + } else if (card && Object.hasOwn(card, '$id')) { + method = card as PaymentMethodData; + } + } selectedPaymentMethodId = method.$id; } isBackup @@ -97,6 +118,9 @@ { @@ -48,7 +57,24 @@ try { if (paymentMethodId === null) { try { - const method = await submitStripeCard(name, $organization.$id); + if (showState && !state) { + throw Error('Please select a state'); + } + let method: PaymentMethodData; + if (showState) { + method = await setPaymentMethod(paymentMethod.id, name, state); + } else { + const card = await submitStripeCard(name, $organization.$id); + if (card && Object.hasOwn(card, 'id')) { + if ((card as PaymentMethod).card.country === 'US') { + paymentMethod = card as PaymentMethod; + showState = true; + return; + } + } else if (card && Object.hasOwn(card, '$id')) { + method = card as PaymentMethodData; + } + } const card = await sdk.forConsole.billing.getPaymentMethod(method.$id); if (card?.last4) { paymentMethodId = card.$id; @@ -131,6 +157,9 @@ import { WizardStep } from '$lib/layout'; import { onMount } from 'svelte'; - import type { PaymentList } from '$lib/sdk/billing'; + import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing'; import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; - import { isStripeInitialized, submitStripeCard } from '$lib/stores/stripe'; + import { isStripeInitialized, setPaymentMethod, submitStripeCard } from '$lib/stores/stripe'; import { sdk } from '$lib/stores/sdk'; import { PaymentBoxes } from '$lib/components/billing'; import { addCreditWizardStore } from '../store'; import { organization } from '$lib/stores/organization'; + import type { PaymentMethod } from '@stripe/stripe-js'; let methods: PaymentList; let name: string; + let showState: boolean = false; + let state: string = ''; + let paymentMethod: PaymentMethod | null = null; onMount(async () => { methods = await sdk.forConsole.billing.listPaymentMethods(); @@ -21,7 +25,24 @@ async function handleSubmit() { try { - const method = await submitStripeCard(name, $organization.$id); + if (showState && !state) { + throw Error('Please select a state'); + } + let method: PaymentMethodData; + if (showState) { + method = await setPaymentMethod(paymentMethod.id, name, state); + } else { + const card = await submitStripeCard(name, $organization.$id); + if (card && Object.hasOwn(card, 'id')) { + if ((card as PaymentMethod).card.country === 'US') { + paymentMethod = card as PaymentMethod; + showState = true; + return; + } + } else if (card && Object.hasOwn(card, '$id')) { + method = card as PaymentMethodData; + } + } $addCreditWizardStore.paymentMethodId = method.$id; invalidate(Dependencies.PAYMENT_METHODS); } catch (e) { @@ -45,6 +66,9 @@

Payment method