From f41f0c61c6ce0d0c8716eb85649c4ceec1d73ed5 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 09:36:45 +0100 Subject: [PATCH 01/19] feat(billing): merge credit cards and billing details into unified section - Combine credit cards and billing info into single BlockContent - Move "Add new card" button inside credit cards subsection - Add divider between credit cards and billing information - Update section titles for better hierarchy Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details/billing-details.tsx | 7 +- .../page-organization-billing.tsx | 127 ++++++++++-------- 2 files changed, 72 insertions(+), 62 deletions(-) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx index e6fa7c49f70..135f503e240 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx @@ -2,7 +2,7 @@ import { type BillingInfoRequest } from 'qovery-typescript-axios' import { type FormEventHandler } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { type Value } from '@qovery/shared/interfaces' -import { BlockContent, Button, InputSelect, InputText, LoaderSpinner } from '@qovery/shared/ui' +import { Button, InputSelect, InputText, LoaderSpinner } from '@qovery/shared/ui' export interface BillingDetailsProps { onSubmit: FormEventHandler @@ -15,7 +15,8 @@ export function BillingDetails(props: BillingDetailsProps) { const { control, formState } = useFormContext() return ( - + <> +

Billing information

{props.loadingBillingInfos ? (
@@ -178,7 +179,7 @@ export function BillingDetails(props: BillingDetailsProps) {
)} -
+ ) } diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index e015e0028b9..97a872cabd3 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -22,69 +22,78 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { return (
-
- Payment method - -
+ Payment method - - {props.creditCardLoading && props.creditCards.length === 0 ? ( -
- + + {/* Credit cards section */} +
+
+

Credit cards

+
- ) : props.creditCards.length > 0 ? ( -
-

- If you want to modify the card, delete the existing one and add a new. -

- {props.creditCards.map((creditCard) => ( -
- - - -
- ))} -
- ) : ( -
- -

- No credit cards found.
Please add one. -

-
- )} - + + + +
+ ))} +
+ ) : ( +
+ +

+ No credit cards found.
Please add one. +

+
+ )} +
+ + {/* Divider */} +
- + {/* Billing details section */} + +
) From 5a2ba562b1dcf82380c67549fd58a73a62a6e3e6 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 09:50:12 +0100 Subject: [PATCH 02/19] feat(billing): integrate credit card form into billing details - Add credit card fields directly in billing details form - Remove modal for adding cards - Clicking "Add new card" scrolls to and activates card fields in form - Form submission handles both billing info and card tokenization - Add cancel button to hide card fields - All validation happens in single unified form Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details-feature.tsx | 111 ++++++++++++++++-- .../page-organization-billing-feature.tsx | 24 ++-- .../billing-details/billing-details.tsx | 75 +++++++++++- .../page-organization-billing.tsx | 10 +- 4 files changed, 195 insertions(+), 25 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx index d278e8be709..50b28edaaf9 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx @@ -1,19 +1,35 @@ +import type FieldContainer from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' +import type CbInstance from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' import { type BillingInfoRequest } from 'qovery-typescript-axios' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useParams } from 'react-router-dom' -import { useBillingInfo, useEditBillingInfo } from '@qovery/domains/organizations/feature' +import { useAddCreditCard, useBillingInfo, useEditBillingInfo } from '@qovery/domains/organizations/feature' import { countries } from '@qovery/shared/enums' import { type Value } from '@qovery/shared/interfaces' -import { IconFlag } from '@qovery/shared/ui' +import { IconFlag, toastError } from '@qovery/shared/ui' +import { loadChargebee } from '@qovery/shared/util-payment' +import { type SerializedError } from '@qovery/shared/utils' import BillingDetails from '../../../ui/page-organization-billing/billing-details/billing-details' -export function BillingDetailsFeature() { +export interface BillingDetailsFeatureProps { + showAddCard?: boolean + onShowAddCardChange?: (show: boolean) => void +} + +export function BillingDetailsFeature(props: BillingDetailsFeatureProps) { const { organizationId = '' } = useParams() const [editInProcess, setEditInProcess] = useState(false) const [countryValues, setCountryValues] = useState([]) + const [cbInstance, setCbInstance] = useState(null) + const [isCardReady, setIsCardReady] = useState(false) + const cardRef = useRef(null) + const showAddCard = props.showAddCard ?? false + const { data: billingInfo, isLoading: isLoadingBillingInfo } = useBillingInfo({ organizationId }) const { mutateAsync: editBillingInfo } = useEditBillingInfo() + const { mutateAsync: addCreditCard } = useAddCreditCard() + const methods = useForm({ mode: 'onChange', defaultValues: { @@ -30,24 +46,89 @@ export function BillingDetailsFeature() { }, }) - const onSubmit = methods.handleSubmit(async (data) => { - if (organizationId) { - setEditInProcess(true) + // Initialize Chargebee when showAddCard becomes true + useEffect(() => { + if (!showAddCard) return + + let mounted = true + const initializeChargebee = async () => { try { - const response = await editBillingInfo({ + const instance = await loadChargebee() + + if (!mounted) { + return + } + + setCbInstance(instance) + } catch (error) { + console.error('Failed to initialize Chargebee:', error) + } + } + + initializeChargebee() + + return () => { + mounted = false + } + }, [showAddCard]) + + const onSubmit = methods.handleSubmit(async (data) => { + if (!organizationId) return + + setEditInProcess(true) + + try { + // If user has added card data, tokenize it first + if (showAddCard && isCardReady && cardRef.current) { + const tokenData = await cardRef.current.tokenize({}) + + if (!tokenData.token) { + throw new Error('No token returned from Chargebee') + } + + // Save the credit card + await addCreditCard({ organizationId, - billingInfoRequest: data, + creditCardRequest: { + token: tokenData.token, + cvv: '', + number: `****${tokenData.card?.last4 || ''}`, + expiry_year: tokenData.card?.expiry_year || 0, + expiry_month: tokenData.card?.expiry_month || 0, + }, }) - methods.reset(response as BillingInfoRequest) - } catch (error) { - console.error(error) + + // Reset card state after successful save + props.onShowAddCardChange?.(false) + setIsCardReady(false) + setCbInstance(null) } + // Save billing info + const response = await editBillingInfo({ + organizationId, + billingInfoRequest: data, + }) + methods.reset(response as BillingInfoRequest) + } catch (error) { + console.error(error) + toastError(error as unknown as SerializedError) + } finally { setEditInProcess(false) } }) + const handleAddCard = () => { + props.onShowAddCardChange?.(true) + } + + const handleCancelAddCard = () => { + props.onShowAddCardChange?.(false) + setIsCardReady(false) + setCbInstance(null) + } + useEffect(() => { setCountryValues( countries.map((country) => ({ label: country.name, value: country.code, icon: })) @@ -65,6 +146,12 @@ export function BillingDetailsFeature() { countryValues={countryValues} loadingBillingInfos={isLoadingBillingInfo} editInProcess={editInProcess} + showAddCard={showAddCard} + cbInstance={cbInstance} + cardRef={cardRef} + onCardReady={() => setIsCardReady(true)} + onAddCard={handleAddCard} + onCancelAddCard={handleCancelAddCard} /> ) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index 6cd6484f488..4676b4eb735 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -1,28 +1,34 @@ import { type CreditCard } from 'qovery-typescript-axios' +import { useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useParams } from 'react-router-dom' import { useCreditCards, useDeleteCreditCard } from '@qovery/domains/organizations/feature' -import { AddCreditCardModalFeature, type CreditCardFormValues } from '@qovery/shared/console-shared' -import { useModal, useModalConfirmation } from '@qovery/shared/ui' +import { type CreditCardFormValues } from '@qovery/shared/console-shared' +import { useModalConfirmation } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/util-hooks' import PageOrganizationBilling from '../../ui/page-organization-billing/page-organization-billing' export function PageOrganizationBillingFeature() { useDocumentTitle('Billing details - Organization settings') const { organizationId = '' } = useParams() - const { openModal } = useModal() const { openModalConfirmation } = useModalConfirmation() const { data: creditCards = [], isLoading: isLoadingCreditCards } = useCreditCards({ organizationId }) const { mutateAsync: deleteCreditCard } = useDeleteCreditCard() + const [showAddCard, setShowAddCard] = useState(false) const methods = useForm({ mode: 'onChange', }) - const openNewCreditCardModal = () => { - openModal({ - content: , - }) + const handleAddCard = () => { + setShowAddCard(true) + // Scroll to billing details form after a short delay to let the state update + setTimeout(() => { + const billingForm = document.querySelector('[data-billing-details-form]') + if (billingForm) { + billingForm.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + }, 100) } const onDeleteCreditCard = (creditCard: CreditCard) => { @@ -40,9 +46,11 @@ export function PageOrganizationBillingFeature() { ) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx index 135f503e240..30c4b323b45 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx @@ -1,14 +1,24 @@ +import { CardCVV, CardComponent, CardExpiry, CardNumber, Provider } from '@chargebee/chargebee-js-react-wrapper' +import type FieldContainer from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' +import type CbInstance from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' import { type BillingInfoRequest } from 'qovery-typescript-axios' -import { type FormEventHandler } from 'react' +import { type FormEventHandler, type RefObject } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { type Value } from '@qovery/shared/interfaces' -import { Button, InputSelect, InputText, LoaderSpinner } from '@qovery/shared/ui' +import { Button, Icon, InputSelect, InputText, LoaderSpinner } from '@qovery/shared/ui' +import { fieldStyles } from '@qovery/shared/util-payment' export interface BillingDetailsProps { onSubmit: FormEventHandler loadingBillingInfos?: boolean editInProcess?: boolean countryValues?: Value[] + showAddCard?: boolean + cbInstance?: CbInstance | null + cardRef?: RefObject + onCardReady?: () => void + onAddCard?: () => void + onCancelAddCard?: () => void } export function BillingDetails(props: BillingDetailsProps) { @@ -165,6 +175,67 @@ export function BillingDetails(props: BillingDetailsProps) { )} /> + + {/* Credit card section */} + {props.showAddCard && ( +
+
+

Add credit card

+ +
+ + {!props.cbInstance ? ( +
+ +
+ ) : ( + + +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ )} +
+ )} + + {!props.showAddCard && props.onAddCard && ( +
+ +
+ )} +
@@ -92,7 +94,9 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {
{/* Billing details section */} - +
+ +
From 23eb1bf69a8b2b6c685c1ee722468fac79527e5e Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 09:52:57 +0100 Subject: [PATCH 03/19] refactor(billing): move credit card fields to credit cards section - Display Chargebee card fields in credit cards section instead of billing info - When no cards exist and user clicks "Add new card", show card fields in place of empty state - Keep billing information section separate below - Remove duplicate "Add credit card" link from billing section - Add cancel button in card fields section Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details-feature.tsx | 2 + .../billing-details/billing-details.tsx | 110 ++++++++---------- .../page-organization-billing.tsx | 12 +- 3 files changed, 59 insertions(+), 65 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx index 50b28edaaf9..ae656adf90d 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx @@ -15,6 +15,7 @@ import BillingDetails from '../../../ui/page-organization-billing/billing-detail export interface BillingDetailsFeatureProps { showAddCard?: boolean onShowAddCardChange?: (show: boolean) => void + showOnlyCardFields?: boolean } export function BillingDetailsFeature(props: BillingDetailsFeatureProps) { @@ -152,6 +153,7 @@ export function BillingDetailsFeature(props: BillingDetailsFeatureProps) { onCardReady={() => setIsCardReady(true)} onAddCard={handleAddCard} onCancelAddCard={handleCancelAddCard} + showOnlyCardFields={props.showOnlyCardFields} /> ) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx index 30c4b323b45..c361b98bcaa 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx @@ -19,11 +19,61 @@ export interface BillingDetailsProps { onCardReady?: () => void onAddCard?: () => void onCancelAddCard?: () => void + showOnlyCardFields?: boolean } export function BillingDetails(props: BillingDetailsProps) { const { control, formState } = useFormContext() + // If we only show card fields, render only the Chargebee form + if (props.showOnlyCardFields && props.showAddCard) { + return ( + <> +
+

Add credit card

+ +
+ + {!props.cbInstance ? ( +
+ +
+ ) : ( + + +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ )} + + ) + } + return ( <>

Billing information

@@ -176,66 +226,6 @@ export function BillingDetails(props: BillingDetailsProps) { />
- {/* Credit card section */} - {props.showAddCard && ( -
-
-

Add credit card

- -
- - {!props.cbInstance ? ( -
- -
- ) : ( - - -
- - -
-
-
- - -
-
- - -
-
-
-
- )} -
- )} - - {!props.showAddCard && props.onAddCard && ( -
- -
- )} -
- {props.creditCardLoading && props.creditCards.length === 0 ? ( + {props.creditCardLoading && props.creditCards.length === 0 && !props.showAddCard ? (
@@ -80,6 +80,10 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { ))} + ) : props.showAddCard ? ( +
+ +
) : (
@@ -93,10 +97,8 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { {/* Divider */}
- {/* Billing details section */} -
- -
+ {/* Billing information section */} +
From 05a985984f9a16dcb2d33921f451e648b1509227 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 09:58:05 +0100 Subject: [PATCH 04/19] fix(billing): unify form state for credit card and billing info - Move all form logic to PageOrganizationBillingFeature parent component - Use single FormProvider for both credit card and billing fields - Fix issue where card tokenization wasn't working due to separate form instances - Credit card fields now properly share cardRef and state with submit handler - Simplify child components to pure UI components This fixes the bug where clicking Save only saved billing info but not the card. Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details-feature.tsx | 161 ++---------------- .../page-organization-billing-feature.tsx | 150 ++++++++++++++-- .../billing-details/billing-details.tsx | 67 +------- .../page-organization-billing.tsx | 70 +++++++- 4 files changed, 217 insertions(+), 231 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx index ae656adf90d..7a0ac9ee845 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx @@ -1,161 +1,22 @@ -import type FieldContainer from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' -import type CbInstance from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' -import { type BillingInfoRequest } from 'qovery-typescript-axios' -import { useEffect, useRef, useState } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import { useParams } from 'react-router-dom' -import { useAddCreditCard, useBillingInfo, useEditBillingInfo } from '@qovery/domains/organizations/feature' -import { countries } from '@qovery/shared/enums' +import { type FormEventHandler } from 'react' import { type Value } from '@qovery/shared/interfaces' -import { IconFlag, toastError } from '@qovery/shared/ui' -import { loadChargebee } from '@qovery/shared/util-payment' -import { type SerializedError } from '@qovery/shared/utils' import BillingDetails from '../../../ui/page-organization-billing/billing-details/billing-details' export interface BillingDetailsFeatureProps { - showAddCard?: boolean - onShowAddCardChange?: (show: boolean) => void - showOnlyCardFields?: boolean + countryValues?: Value[] + loadingBillingInfos?: boolean + editInProcess?: boolean + onSubmit?: FormEventHandler } export function BillingDetailsFeature(props: BillingDetailsFeatureProps) { - const { organizationId = '' } = useParams() - const [editInProcess, setEditInProcess] = useState(false) - const [countryValues, setCountryValues] = useState([]) - const [cbInstance, setCbInstance] = useState(null) - const [isCardReady, setIsCardReady] = useState(false) - const cardRef = useRef(null) - const showAddCard = props.showAddCard ?? false - - const { data: billingInfo, isLoading: isLoadingBillingInfo } = useBillingInfo({ organizationId }) - const { mutateAsync: editBillingInfo } = useEditBillingInfo() - const { mutateAsync: addCreditCard } = useAddCreditCard() - - const methods = useForm({ - mode: 'onChange', - defaultValues: { - city: '', - address: '', - state: '', - company: '', - zip: '', - email: '', - first_name: '', - last_name: '', - vat_number: '', - country_code: '', - }, - }) - - // Initialize Chargebee when showAddCard becomes true - useEffect(() => { - if (!showAddCard) return - - let mounted = true - - const initializeChargebee = async () => { - try { - const instance = await loadChargebee() - - if (!mounted) { - return - } - - setCbInstance(instance) - } catch (error) { - console.error('Failed to initialize Chargebee:', error) - } - } - - initializeChargebee() - - return () => { - mounted = false - } - }, [showAddCard]) - - const onSubmit = methods.handleSubmit(async (data) => { - if (!organizationId) return - - setEditInProcess(true) - - try { - // If user has added card data, tokenize it first - if (showAddCard && isCardReady && cardRef.current) { - const tokenData = await cardRef.current.tokenize({}) - - if (!tokenData.token) { - throw new Error('No token returned from Chargebee') - } - - // Save the credit card - await addCreditCard({ - organizationId, - creditCardRequest: { - token: tokenData.token, - cvv: '', - number: `****${tokenData.card?.last4 || ''}`, - expiry_year: tokenData.card?.expiry_year || 0, - expiry_month: tokenData.card?.expiry_month || 0, - }, - }) - - // Reset card state after successful save - props.onShowAddCardChange?.(false) - setIsCardReady(false) - setCbInstance(null) - } - - // Save billing info - const response = await editBillingInfo({ - organizationId, - billingInfoRequest: data, - }) - methods.reset(response as BillingInfoRequest) - } catch (error) { - console.error(error) - toastError(error as unknown as SerializedError) - } finally { - setEditInProcess(false) - } - }) - - const handleAddCard = () => { - props.onShowAddCardChange?.(true) - } - - const handleCancelAddCard = () => { - props.onShowAddCardChange?.(false) - setIsCardReady(false) - setCbInstance(null) - } - - useEffect(() => { - setCountryValues( - countries.map((country) => ({ label: country.name, value: country.code, icon: })) - ) - }, [setCountryValues]) - - useEffect(() => { - methods.reset(billingInfo as BillingInfoRequest) - }, [billingInfo, methods]) - return ( - - setIsCardReady(true)} - onAddCard={handleAddCard} - onCancelAddCard={handleCancelAddCard} - showOnlyCardFields={props.showOnlyCardFields} - /> - + ) } diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index 4676b4eb735..ca19905401a 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -1,11 +1,22 @@ -import { type CreditCard } from 'qovery-typescript-axios' -import { useState } from 'react' +import type FieldContainer from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' +import type CbInstance from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' +import { type BillingInfoRequest, type CreditCard } from 'qovery-typescript-axios' +import { useEffect, useRef, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useParams } from 'react-router-dom' -import { useCreditCards, useDeleteCreditCard } from '@qovery/domains/organizations/feature' -import { type CreditCardFormValues } from '@qovery/shared/console-shared' -import { useModalConfirmation } from '@qovery/shared/ui' +import { + useAddCreditCard, + useBillingInfo, + useCreditCards, + useDeleteCreditCard, + useEditBillingInfo, +} from '@qovery/domains/organizations/feature' +import { countries } from '@qovery/shared/enums' +import { type Value } from '@qovery/shared/interfaces' +import { IconFlag, toastError, useModalConfirmation } from '@qovery/shared/ui' +import { loadChargebee } from '@qovery/shared/util-payment' import { useDocumentTitle } from '@qovery/shared/util-hooks' +import { type SerializedError } from '@qovery/shared/utils' import PageOrganizationBilling from '../../ui/page-organization-billing/page-organization-billing' export function PageOrganizationBillingFeature() { @@ -14,23 +25,133 @@ export function PageOrganizationBillingFeature() { const { openModalConfirmation } = useModalConfirmation() const { data: creditCards = [], isLoading: isLoadingCreditCards } = useCreditCards({ organizationId }) const { mutateAsync: deleteCreditCard } = useDeleteCreditCard() + const { data: billingInfo, isLoading: isLoadingBillingInfo } = useBillingInfo({ organizationId }) + const { mutateAsync: editBillingInfo } = useEditBillingInfo() + const { mutateAsync: addCreditCard } = useAddCreditCard() + const [showAddCard, setShowAddCard] = useState(false) + const [editInProcess, setEditInProcess] = useState(false) + const [countryValues, setCountryValues] = useState([]) + const [cbInstance, setCbInstance] = useState(null) + const [isCardReady, setIsCardReady] = useState(false) + const cardRef = useRef(null) - const methods = useForm({ + const methods = useForm({ mode: 'onChange', + defaultValues: { + city: '', + address: '', + state: '', + company: '', + zip: '', + email: '', + first_name: '', + last_name: '', + vat_number: '', + country_code: '', + }, }) + // Initialize Chargebee when showAddCard becomes true + useEffect(() => { + if (!showAddCard) return + + let mounted = true + + const initializeChargebee = async () => { + try { + const instance = await loadChargebee() + + if (!mounted) { + return + } + + setCbInstance(instance) + } catch (error) { + console.error('Failed to initialize Chargebee:', error) + } + } + + initializeChargebee() + + return () => { + mounted = false + } + }, [showAddCard]) + + useEffect(() => { + setCountryValues( + countries.map((country) => ({ label: country.name, value: country.code, icon: })) + ) + }, []) + + useEffect(() => { + methods.reset(billingInfo as BillingInfoRequest) + }, [billingInfo, methods]) + const handleAddCard = () => { setShowAddCard(true) - // Scroll to billing details form after a short delay to let the state update + // Scroll to credit card section setTimeout(() => { - const billingForm = document.querySelector('[data-billing-details-form]') - if (billingForm) { - billingForm.scrollIntoView({ behavior: 'smooth', block: 'start' }) + const cardSection = document.querySelector('[data-credit-card-section]') + if (cardSection) { + cardSection.scrollIntoView({ behavior: 'smooth', block: 'start' }) } }, 100) } + const handleCancelAddCard = () => { + setShowAddCard(false) + setIsCardReady(false) + setCbInstance(null) + } + + const onSubmit = methods.handleSubmit(async (data) => { + if (!organizationId) return + + setEditInProcess(true) + + try { + // If user has added card data, tokenize it first + if (showAddCard && isCardReady && cardRef.current) { + const tokenData = await cardRef.current.tokenize({}) + + if (!tokenData.token) { + throw new Error('No token returned from Chargebee') + } + + // Save the credit card + await addCreditCard({ + organizationId, + creditCardRequest: { + token: tokenData.token, + cvv: '', + number: `****${tokenData.card?.last4 || ''}`, + expiry_year: tokenData.card?.expiry_year || 0, + expiry_month: tokenData.card?.expiry_month || 0, + }, + }) + + // Reset card state after successful save + setShowAddCard(false) + setIsCardReady(false) + setCbInstance(null) + } + + // Save billing info + const response = await editBillingInfo({ + organizationId, + billingInfoRequest: data, + }) + methods.reset(response as BillingInfoRequest) + } catch (error) { + console.error(error) + toastError(error as unknown as SerializedError) + } finally { + setEditInProcess(false) + } + }) + const onDeleteCreditCard = (creditCard: CreditCard) => { openModalConfirmation({ title: 'Delete credit card', @@ -50,7 +171,14 @@ export function PageOrganizationBillingFeature() { onDeleteCard={onDeleteCreditCard} creditCardLoading={isLoadingCreditCards} showAddCard={showAddCard} - onShowAddCardChange={setShowAddCard} + onCancelAddCard={handleCancelAddCard} + cbInstance={cbInstance} + cardRef={cardRef} + onCardReady={() => setIsCardReady(true)} + countryValues={countryValues} + loadingBillingInfos={isLoadingBillingInfo} + editInProcess={editInProcess} + onSubmit={onSubmit} /> ) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx index c361b98bcaa..d7fd86ac261 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx @@ -1,79 +1,19 @@ -import { CardCVV, CardComponent, CardExpiry, CardNumber, Provider } from '@chargebee/chargebee-js-react-wrapper' -import type FieldContainer from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' -import type CbInstance from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' import { type BillingInfoRequest } from 'qovery-typescript-axios' -import { type FormEventHandler, type RefObject } from 'react' +import { type FormEventHandler } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { type Value } from '@qovery/shared/interfaces' -import { Button, Icon, InputSelect, InputText, LoaderSpinner } from '@qovery/shared/ui' -import { fieldStyles } from '@qovery/shared/util-payment' +import { Button, InputSelect, InputText, LoaderSpinner } from '@qovery/shared/ui' export interface BillingDetailsProps { - onSubmit: FormEventHandler + onSubmit?: FormEventHandler loadingBillingInfos?: boolean editInProcess?: boolean countryValues?: Value[] - showAddCard?: boolean - cbInstance?: CbInstance | null - cardRef?: RefObject - onCardReady?: () => void - onAddCard?: () => void - onCancelAddCard?: () => void - showOnlyCardFields?: boolean } export function BillingDetails(props: BillingDetailsProps) { const { control, formState } = useFormContext() - // If we only show card fields, render only the Chargebee form - if (props.showOnlyCardFields && props.showAddCard) { - return ( - <> -
-

Add credit card

- -
- - {!props.cbInstance ? ( -
- -
- ) : ( - - -
- - -
-
-
- - -
-
- - -
-
-
-
- )} - - ) - } - return ( <>

Billing information

@@ -225,7 +165,6 @@ export function BillingDetails(props: BillingDetailsProps) { )} />
-
) : props.showAddCard ? ( -
- -
+ <> +
+

Add credit card

+ +
+ + {!props.cbInstance ? ( +
+ +
+ ) : ( + + +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ )} + ) : (
@@ -98,7 +151,12 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {
{/* Billing information section */} - +
From a325ce13ef730ace03efd629acfbacae52cbd79d Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:06:08 +0100 Subject: [PATCH 05/19] feat(billing): make additional fields required and improve validation UX - Make VAT number, address, city, and zip code required fields - Remove "(optional)" labels from now-required fields - Enable Save button at all times (remove disabled state) - Show validation errors on fields when user attempts to submit - Add error messages for all required fields This provides better UX by allowing users to see which fields need to be filled instead of just having a disabled button. Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details/billing-details.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx index d7fd86ac261..86af45e2abf 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx @@ -35,6 +35,7 @@ export function BillingDetails(props: BillingDetailsProps) { label="First name" value={field.value} onChange={field.onChange} + error={formState.errors.first_name?.message} /> )} /> @@ -49,6 +50,7 @@ export function BillingDetails(props: BillingDetailsProps) { label="Last name" value={field.value} onChange={field.onChange} + error={formState.errors.last_name?.message} /> )} /> @@ -70,13 +72,15 @@ export function BillingDetails(props: BillingDetailsProps) { ( )} /> @@ -92,19 +96,22 @@ export function BillingDetails(props: BillingDetailsProps) { label="Billing email" value={field.value} onChange={field.onChange} + error={formState.errors.email?.message} /> )} /> ( )} /> @@ -112,26 +119,30 @@ export function BillingDetails(props: BillingDetailsProps) { ( )} /> ( )} /> @@ -170,7 +181,6 @@ export function BillingDetails(props: BillingDetailsProps) { data-testid="submit-button" type="submit" size="lg" - disabled={!formState.isValid} loading={props.editInProcess} onClick={props.onSubmit as () => void} > From 255fce2f1f092de67cea01241885942afc56fac1 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:07:36 +0100 Subject: [PATCH 06/19] feat(billing): make country field required - Add validation rule for country_code field - Remove "(optional)" label from Country field - Add error message display for country validation Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details/billing-details.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx index 86af45e2abf..bf8f6035517 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx @@ -151,14 +151,16 @@ export function BillingDetails(props: BillingDetailsProps) { ( )} /> From 8b7a224496e5b3855ff42a7a0c31c9995cbb7d9b Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:11:18 +0100 Subject: [PATCH 07/19] fix(billing): save credit card only after billing info validation succeeds - Reorder submission flow: validate and save billing info first - Add credit card only if billing info save succeeds - Prevents card from being added when VAT number or other fields are invalid - If billing validation fails, card tokenization doesn't happen Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index ca19905401a..1f0e4782fb8 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -112,7 +112,14 @@ export function PageOrganizationBillingFeature() { setEditInProcess(true) try { - // If user has added card data, tokenize it first + // Save billing info first to validate it (especially VAT number) + const response = await editBillingInfo({ + organizationId, + billingInfoRequest: data, + }) + methods.reset(response as BillingInfoRequest) + + // Only if billing info is valid, then add the credit card if (showAddCard && isCardReady && cardRef.current) { const tokenData = await cardRef.current.tokenize({}) @@ -137,13 +144,6 @@ export function PageOrganizationBillingFeature() { setIsCardReady(false) setCbInstance(null) } - - // Save billing info - const response = await editBillingInfo({ - organizationId, - billingInfoRequest: data, - }) - methods.reset(response as BillingInfoRequest) } catch (error) { console.error(error) toastError(error as unknown as SerializedError) From 07d99914920111a2f0dcee2e93d244875dd6e3d3 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:19:46 +0100 Subject: [PATCH 08/19] feat(billing): add edit button for credit cards and improve UX - Hide "Add new card" button when a card already exists - Add "Edit" button (pen icon) next to delete button on existing cards - Remove instructional text about deleting cards - Clicking edit opens the card fields to add/replace the card Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index 2c750b8fbbe..5cf0d62f346 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -44,10 +44,12 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {

Credit cards

- + {props.creditCards.length === 0 && ( + + )}
{props.creditCardLoading && props.creditCards.length === 0 && !props.showAddCard ? ( @@ -56,9 +58,6 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {
) : props.creditCards.length > 0 ? (
-

- If you want to modify the card, delete the existing one and add a new. -

{props.creditCards.map((creditCard) => (
+ @@ -56,52 +56,58 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {
- ) : props.creditCards.length > 0 ? ( -
- {props.creditCards.map((creditCard) => ( -
- - - - + ) : ( + <> + {/* Existing cards */} + {props.creditCards.length > 0 && !props.showAddCard && ( +
+ {props.creditCards.map((creditCard) => ( +
+ + + + +
+ ))}
- ))} -
- ) : props.showAddCard ? ( + )} + + {/* Add/Edit card form */} + {props.showAddCard && ( <>

Add credit card

@@ -145,13 +151,18 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { )} - ) : ( -
- -

- No credit cards found.
Please add one. -

-
+ )} + + {/* Empty state */} + {!props.showAddCard && props.creditCards.length === 0 && ( +
+ +

+ No credit cards found.
Please add one. +

+
+ )} + )}
From 90462a9fa367cd36379abb307f98f77c03cf68a2 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:34:19 +0100 Subject: [PATCH 10/19] refactor(billing): clean up code and improve UX - Remove console.error statements - Change "Credit cards" to "Credit card" (singular) - Move "Add new card" button to empty state and center it - Update empty state text to singular form Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 3 +-- .../page-organization-billing.tsx | 20 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index bbef4805329..7d36270a8aa 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -69,7 +69,7 @@ export function PageOrganizationBillingFeature() { setCbInstance(instance) } catch (error) { - console.error('Failed to initialize Chargebee:', error) + // Error already handled by loadChargebee } } @@ -154,7 +154,6 @@ export function PageOrganizationBillingFeature() { setEditingCardId(null) } } catch (error) { - console.error(error) toastError(error as unknown as SerializedError) } finally { setEditInProcess(false) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index ab3c13ab478..f9811dd4ee4 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -40,16 +40,10 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { Payment method - {/* Credit cards section */} + {/* Credit card section */}
-
-

Credit cards

- {props.creditCards.length === 0 && ( - - )} +
+

Credit card

{props.creditCardLoading && props.creditCards.length === 0 && !props.showAddCard ? ( @@ -158,8 +152,14 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {

- No credit cards found.
Please add one. + No credit card found.
Please add one.

+
+ +
)} From baa157e169593d2c4b23b64d648ef4a70f46c4b9 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:35:34 +0100 Subject: [PATCH 11/19] style(billing): change add card button to outline variant Co-Authored-By: Claude Sonnet 4.5 --- .../ui/page-organization-billing/page-organization-billing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index f9811dd4ee4..c7f9549ba5b 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -155,7 +155,7 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { No credit card found.
Please add one.

- From b4d9b671e75198a911f00e0463bbf2df8363fac2 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:40:54 +0100 Subject: [PATCH 12/19] chore(billing): apply linter formatting - Reorder imports alphabetically - Fix indentation for better code alignment Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 2 +- .../page-organization-billing.tsx | 90 ++++++++++--------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index 7d36270a8aa..5c99d6beb16 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -14,8 +14,8 @@ import { import { countries } from '@qovery/shared/enums' import { type Value } from '@qovery/shared/interfaces' import { IconFlag, toastError, useModalConfirmation } from '@qovery/shared/ui' -import { loadChargebee } from '@qovery/shared/util-payment' import { useDocumentTitle } from '@qovery/shared/util-hooks' +import { loadChargebee } from '@qovery/shared/util-payment' import { type SerializedError } from '@qovery/shared/utils' import PageOrganizationBilling from '../../ui/page-organization-billing/page-organization-billing' diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index c7f9549ba5b..f441dfb5091 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -102,49 +102,49 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { {/* Add/Edit card form */} {props.showAddCard && ( - <> -
-

Add credit card

- -
+ <> +
+

Add credit card

+ +
- {!props.cbInstance ? ( -
- -
- ) : ( - - -
- - -
-
-
- - -
-
- - -
+ {!props.cbInstance ? ( +
+
- - - )} - + ) : ( + + +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ )} + )} {/* Empty state */} @@ -155,7 +155,13 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { No credit card found.
Please add one.

- From 3fac36aec66b52a45dcc4cad12dd7d3c44d58682 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 10:57:40 +0100 Subject: [PATCH 13/19] fix(billing): use inline type imports as per project standards - Change `import type X` to `import { type default as X }` - Follows React & TypeScript Standards rule: "ALWAYS use inline type imports" Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 4 ++-- .../page-organization-billing/page-organization-billing.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index 5c99d6beb16..d9962defc71 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -1,5 +1,5 @@ -import type FieldContainer from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' -import type CbInstance from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' +import { type default as FieldContainer } from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' +import { type default as CbInstance } from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' import { type BillingInfoRequest, type CreditCard } from 'qovery-typescript-axios' import { useEffect, useRef, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index f441dfb5091..75f3a202035 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -1,6 +1,6 @@ import { CardCVV, CardComponent, CardExpiry, CardNumber, Provider } from '@chargebee/chargebee-js-react-wrapper' -import type FieldContainer from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' -import type CbInstance from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' +import { type default as FieldContainer } from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' +import { type default as CbInstance } from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' import { type CreditCard } from 'qovery-typescript-axios' import { type FormEventHandler, type RefObject } from 'react' import { type Value } from '@qovery/shared/interfaces' From 032c96167c346d4bd46cffb8288371b9ec31dcac Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 11:01:53 +0100 Subject: [PATCH 14/19] refactor(billing): remove comments and optimize useEffect usage - Remove all inline comments added during development - Replace countryValues useEffect with direct initialization - Clean up unnecessary comments in try-catch blocks - Optimize code for better readability without comments Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 22 ++++++------------- .../page-organization-billing.tsx | 6 ----- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index d9962defc71..9c10c3be90c 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -31,12 +31,17 @@ export function PageOrganizationBillingFeature() { const [showAddCard, setShowAddCard] = useState(false) const [editInProcess, setEditInProcess] = useState(false) - const [countryValues, setCountryValues] = useState([]) const [cbInstance, setCbInstance] = useState(null) const [isCardReady, setIsCardReady] = useState(false) const [editingCardId, setEditingCardId] = useState(null) const cardRef = useRef(null) + const countryValues = countries.map((country) => ({ + label: country.name, + value: country.code, + icon: , + })) + const methods = useForm({ mode: 'onChange', defaultValues: { @@ -53,7 +58,6 @@ export function PageOrganizationBillingFeature() { }, }) - // Initialize Chargebee when showAddCard becomes true useEffect(() => { if (!showAddCard) return @@ -69,7 +73,7 @@ export function PageOrganizationBillingFeature() { setCbInstance(instance) } catch (error) { - // Error already handled by loadChargebee + return } } @@ -80,12 +84,6 @@ export function PageOrganizationBillingFeature() { } }, [showAddCard]) - useEffect(() => { - setCountryValues( - countries.map((country) => ({ label: country.name, value: country.code, icon: })) - ) - }, []) - useEffect(() => { methods.reset(billingInfo as BillingInfoRequest) }, [billingInfo, methods]) @@ -93,7 +91,6 @@ export function PageOrganizationBillingFeature() { const handleAddCard = (cardId?: string) => { setShowAddCard(true) setEditingCardId(cardId || null) - // Scroll to credit card section setTimeout(() => { const cardSection = document.querySelector('[data-credit-card-section]') if (cardSection) { @@ -115,14 +112,12 @@ export function PageOrganizationBillingFeature() { setEditInProcess(true) try { - // Save billing info first to validate it (especially VAT number) const response = await editBillingInfo({ organizationId, billingInfoRequest: data, }) methods.reset(response as BillingInfoRequest) - // Only if billing info is valid, then add the credit card if (showAddCard && isCardReady && cardRef.current) { const tokenData = await cardRef.current.tokenize({}) @@ -130,7 +125,6 @@ export function PageOrganizationBillingFeature() { throw new Error('No token returned from Chargebee') } - // Save the new credit card await addCreditCard({ organizationId, creditCardRequest: { @@ -142,12 +136,10 @@ export function PageOrganizationBillingFeature() { }, }) - // If we were editing a card, delete the old one after successfully adding the new one if (editingCardId) { await deleteCreditCard({ organizationId, creditCardId: editingCardId }) } - // Reset card state after successful save setShowAddCard(false) setIsCardReady(false) setCbInstance(null) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index 75f3a202035..a6268c90932 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -40,7 +40,6 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { Payment method - {/* Credit card section */}

Credit card

@@ -52,7 +51,6 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {
) : ( <> - {/* Existing cards */} {props.creditCards.length > 0 && !props.showAddCard && (
{props.creditCards.map((creditCard) => ( @@ -100,7 +98,6 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {
)} - {/* Add/Edit card form */} {props.showAddCard && ( <>
@@ -147,7 +144,6 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { )} - {/* Empty state */} {!props.showAddCard && props.creditCards.length === 0 && (
@@ -172,10 +168,8 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) { )}
- {/* Divider */}
- {/* Billing information section */} Date: Fri, 30 Jan 2026 11:04:08 +0100 Subject: [PATCH 15/19] refactor(billing): remove all useEffect hooks - Replace useEffect form reset with useForm 'values' option - Move Chargebee initialization from useEffect to handleAddCard function - Remove useEffect import as it's no longer needed - Zero useEffect hooks remaining Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 53 ++++--------------- 1 file changed, 10 insertions(+), 43 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index 9c10c3be90c..953b1f65ce2 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -1,7 +1,7 @@ import { type default as FieldContainer } from '@chargebee/chargebee-js-react-wrapper/dist/components/FieldContainer' import { type default as CbInstance } from '@chargebee/chargebee-js-types/cb-types/models/cb-instance' import { type BillingInfoRequest, type CreditCard } from 'qovery-typescript-axios' -import { useEffect, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useParams } from 'react-router-dom' import { @@ -44,53 +44,20 @@ export function PageOrganizationBillingFeature() { const methods = useForm({ mode: 'onChange', - defaultValues: { - city: '', - address: '', - state: '', - company: '', - zip: '', - email: '', - first_name: '', - last_name: '', - vat_number: '', - country_code: '', - }, + values: billingInfo as BillingInfoRequest, }) - useEffect(() => { - if (!showAddCard) return - - let mounted = true - - const initializeChargebee = async () => { - try { - const instance = await loadChargebee() - - if (!mounted) { - return - } - - setCbInstance(instance) - } catch (error) { - return - } - } - - initializeChargebee() + const handleAddCard = async (cardId?: string) => { + setShowAddCard(true) + setEditingCardId(cardId || null) - return () => { - mounted = false + try { + const instance = await loadChargebee() + setCbInstance(instance) + } catch (error) { + return } - }, [showAddCard]) - useEffect(() => { - methods.reset(billingInfo as BillingInfoRequest) - }, [billingInfo, methods]) - - const handleAddCard = (cardId?: string) => { - setShowAddCard(true) - setEditingCardId(cardId || null) setTimeout(() => { const cardSection = document.querySelector('[data-credit-card-section]') if (cardSection) { From 4df40f0e68cb20c193634b68f4ca7d0aec1ab51d Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 16:45:18 +0100 Subject: [PATCH 16/19] fix(billing): improve scroll behavior when adding card - Move scroll to after Chargebee initialization completes - Replace setTimeout with requestAnimationFrame for better DOM sync - Prevents strange scroll behavior during async Chargebee loading Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index 953b1f65ce2..6d903d38643 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -54,16 +54,16 @@ export function PageOrganizationBillingFeature() { try { const instance = await loadChargebee() setCbInstance(instance) + + requestAnimationFrame(() => { + const cardSection = document.querySelector('[data-credit-card-section]') + if (cardSection) { + cardSection.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + }) } catch (error) { return } - - setTimeout(() => { - const cardSection = document.querySelector('[data-credit-card-section]') - if (cardSection) { - cardSection.scrollIntoView({ behavior: 'smooth', block: 'start' }) - } - }, 100) } const handleCancelAddCard = () => { From 7c039b1729be25aa3b56e984ff53ef8510ca1f88 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 16:47:10 +0100 Subject: [PATCH 17/19] fix(billing): remove automatic scroll when adding card Remove scroll behavior that was conflicting with dynamic page elements like overdue invoices banner. User already sees the form appear in the same section where they clicked. Co-Authored-By: Claude Sonnet 4.5 --- .../page-organization-billing-feature.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx index 6d903d38643..1ee3aefa089 100644 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx +++ b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/page-organization-billing-feature.tsx @@ -54,13 +54,6 @@ export function PageOrganizationBillingFeature() { try { const instance = await loadChargebee() setCbInstance(instance) - - requestAnimationFrame(() => { - const cardSection = document.querySelector('[data-credit-card-section]') - if (cardSection) { - cardSection.scrollIntoView({ behavior: 'smooth', block: 'start' }) - } - }) } catch (error) { return } From 7c7381dc748b6d21b4d81a4c8929ad42ef0e8959 Mon Sep 17 00:00:00 2001 From: Julien Dan Date: Fri, 30 Jan 2026 16:54:09 +0100 Subject: [PATCH 18/19] refactor(billing): remove useless BillingDetailsFeature wrapper - Remove BillingDetailsFeature component that only passed props through - Use BillingDetails directly in PageOrganizationBilling - Delete unnecessary test file - Simplifies code structure and reduces indirection Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details-feature.spec.tsx | 43 ------------------- .../billing-details-feature.tsx | 23 ---------- .../page-organization-billing.tsx | 4 +- 3 files changed, 2 insertions(+), 68 deletions(-) delete mode 100644 libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.spec.tsx delete mode 100644 libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.spec.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.spec.tsx deleted file mode 100644 index 7b9714f07bc..00000000000 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.spec.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { render } from '__tests__/utils/setup-jest' -import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' -import { type BillingInfoRequest } from 'qovery-typescript-axios' -import BillingDetailsFeature from './billing-details-feature' - -const mockOnSubmit = jest.fn() - -const defaultBillingValues: BillingInfoRequest = { - first_name: 'John', - last_name: 'Doe', - company: 'Qovery', - address: '1 rue de la paix', - city: 'Paris', - state: 'Ile de France', - zip: '75000', - country_code: 'FR', - vat_number: 'FR123456789', - email: 'test@qovery.com', -} - -const countryValues = [ - { label: 'France', value: 'FR' }, - { label: 'United States', value: 'US' }, -] - -describe('BillingDetailsFeature', () => { - it('should render successfully', () => { - const { baseElement } = render( - wrapWithReactHookForm( - , - { - defaultValues: defaultBillingValues, - } - ) - ) - expect(baseElement).toBeTruthy() - }) -}) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx deleted file mode 100644 index 7a0ac9ee845..00000000000 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-feature/billing-details-feature/billing-details-feature.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { type FormEventHandler } from 'react' -import { type Value } from '@qovery/shared/interfaces' -import BillingDetails from '../../../ui/page-organization-billing/billing-details/billing-details' - -export interface BillingDetailsFeatureProps { - countryValues?: Value[] - loadingBillingInfos?: boolean - editInProcess?: boolean - onSubmit?: FormEventHandler -} - -export function BillingDetailsFeature(props: BillingDetailsFeatureProps) { - return ( - - ) -} - -export default BillingDetailsFeature diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx index a6268c90932..c794a25285a 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/page-organization-billing.tsx @@ -15,7 +15,7 @@ import { Section, } from '@qovery/shared/ui' import { fieldStyles } from '@qovery/shared/util-payment' -import BillingDetailsFeature from '../../feature/page-organization-billing-feature/billing-details-feature/billing-details-feature' +import BillingDetails from './billing-details/billing-details' export interface PageOrganizationBillingProps { creditCards: CreditCard[] @@ -170,7 +170,7 @@ export function PageOrganizationBilling(props: PageOrganizationBillingProps) {
- Date: Wed, 4 Feb 2026 16:24:58 +0100 Subject: [PATCH 19/19] fix(billing): prevent field width changes when displaying errors Replace flex-grow with flex-1 min-w-0 to maintain consistent field widths in form rows when error messages appear. Also add items-start to flex containers to prevent vertical stretching. Co-Authored-By: Claude Sonnet 4.5 --- .../billing-details/billing-details.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx index bf8f6035517..da224de8807 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing/billing-details/billing-details.tsx @@ -23,14 +23,14 @@ export function BillingDetails(props: BillingDetailsProps) {
) : ( <> -
+
( (
-
+
( ( ( ( )} /> -
+
( (