Repository: Automattic/wp-calypso
Pull request #99149: Improve billing information loading state
Author: login: StevenDufresne
Labels: name: [Feature] Checkout
description: The checkout screen and process for purchases made on WordPress.com.
Created: 2025-01-31T06:32:56Z | Merged: 2025-03-11T00:49:38Z

Related to #98527

## Proposed Changes

This PR updates the "Billing Information" loading state to reduce the number of visual changes that occur on cart load.

It does so by doing two things:
- Use the return value from `useCachedContactDetailsForCheckoutForm` to determine when the prefill action has been completed and display a loading string in the meantime.
- Hide the sibling `<button>` element .

## Before 
https://github.com/user-attachments/assets/e852e759-1427-426f-aba0-4ae106fd396a

## After
https://github.com/user-attachments/assets/5f48562a-ac30-4a6e-89ce-83373d60802d

## Screenshot

<img width="684" alt="Screenshot 2025-02-13 at 10 44 33 AM" src="https://github.com/user-attachments/assets/c3d92bda-0dc8-4db1-9ee3-bfbe2b09737f" />

## Why are these changes being made?
 
As described in #98527, there are multiple different loading states on cart load. This is a distracting experience.

## Considerations

I go into more detail about things I tried in the [original issue](https://github.com/Automattic/wp-calypso/issues/98527#issuecomment-2608872136). I tried a few other things, like calling the domain contact endpoint earlier on page load. That didn't have much of an effect. The visual state was more or less unchanged. 

Loading it synchronously is the most logical way to remove these intermediate states or maybe have a lazy load system for users who don't start their session on the checkout page. Regardless, [I agree with](https://github.com/Automattic/wp-calypso/issues/98527#issuecomment-2613616772) @sirbrillig that keeping it simple is probably the win.

## Testing Instructions

**Logged out cart**
- Go to [JetPack Pricing](https://cloud.jetpack.com/pricing), click "Get"
- Fill in "Enter your billing information" section.
- Click "Continue to payment".


**Cart without saved contact**
- In a new account, add items to cart.
- Fill in "Enter your billing information" section.
- Click "Continue to payment".

**Cart with saved contact**
- In the account used above, add items to cart
- Expect "Enter your billing information" to show the loading state.
- After a moment, expect to see "Billing Information" completely
- Click "Edit", change some details, reload the page.
- Expect to see the same flow with updates parameters

**Test Pix**

Pix also uses the cached contact details. You can test it using the instructions from https://github.com/Automattic/wp-calypso/pull/86826.

## Pre-merge Checklist

<!--
Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked.

Both the PR author and reviewer are responsible for ensuring the checklist is completed.
-->

- [ ] Has the general commit checklist been followed? (PCYsg-hS-p2)
- [ ] [Have you written new tests](https://wpcalypso.wordpress.com/devdocs/docs/testing/index.md) for your changes?
- [ ] Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
- [ ] Have you checked for TypeScript, React or other console errors?
- [ ] Have you used memoizing on expensive computations? More info in [Memoizing with create-selector](https://github.com/Automattic/wp-calypso/blob/trunk/packages/state-utils/src/create-selector/README.md) and [Using memoizing selectors](https://react-redux.js.org/api/hooks#using-memoizing-selectors) and [Our Approach to Data](https://github.com/Automattic/wp-calypso/blob/trunk/docs/our-approach-to-data.md)
- [ ] Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    - [ ] For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages. 
- [ ] For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?


Diff:
diff --git a/client/my-sites/checkout/src/components/wp-contact-form.tsx b/client/my-sites/checkout/src/components/wp-contact-form.tsx
index 39e2f3647453..be5b83da2b42 100644
--- a/client/my-sites/checkout/src/components/wp-contact-form.tsx
+++ b/client/my-sites/checkout/src/components/wp-contact-form.tsx
@@ -1,12 +1,18 @@
 import { FormStatus, useFormStatus, useIsStepActive } from '@automattic/composite-checkout';
+import { css } from '@emotion/react';
 import styled from '@emotion/styled';
 import { useSelect } from '@wordpress/data';
+import { useTranslate } from 'i18n-calypso';
 import { usePrefillCheckoutContactForm } from '../hooks/use-prefill-checkout-contact-form';
 import { CHECKOUT_STORE } from '../lib/wpcom-store';
 import ContactDetailsContainer from './contact-details-container';
 import type { CountryListItem, ContactDetailsType } from '@automattic/wpcom-checkout';
 
-const BillingFormFields = styled.div`
+interface BillingFormFieldsProps {
+	isLoading: boolean;
+}
+
+const BillingFormFields = styled.div< BillingFormFieldsProps >`
 	margin-bottom: 16px;
 
 	& input,
@@ -41,6 +47,19 @@ const BillingFormFields = styled.div`
 			margin-right: 0;
 		}
 	}
+
+	${ ( props ) =>
+		props.isLoading &&
+		css`
+			+ button {
+				display: none;
+			}
+		` }
+`;
+
+const LoadingText = styled.p`
+	font-size: 14px;
+	color: ${ ( props ) => props.theme.colors.textColor };
 `;
 
 export default function WPContactForm( {
@@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
+			) }
 		</BillingFormFields>
 	);
 }
diff --git a/client/my-sites/checkout/src/hooks/use-cached-contact-details.ts b/client/my-sites/checkout/src/hooks/use-cached-contact-details.ts
index eba3a1e49d30..f92f40451151 100644
--- a/client/my-sites/checkout/src/hooks/use-cached-contact-details.ts
+++ b/client/my-sites/checkout/src/hooks/use-cached-contact-details.ts
@@ -11,11 +11,15 @@ import type {
 const debug = debugFactory( 'calypso:user-cached-contact-details' );
 
 async function fetchCachedContactDetails(): Promise< PossiblyCompleteDomainContactDetails > {
-	const rawData: RawCachedDomainContactDetails = await wpcom.req.get(
-		'/me/domain-contact-information'
-	);
-	debug( 'fetched cached contact details', rawData );
-	return convertSnakeCaseContactDetailsToCamelCase( rawData );
+	try {
+		const rawData: RawCachedDomainContactDetails = await wpcom.req.get(
+			'/me/domain-contact-information'
+		);
+		debug( 'fetched cached contact details', rawData );
+		return convertSnakeCaseContactDetailsToCamelCase( rawData );
+	} catch ( error ) {
+		return Promise.reject( new Error( 'Error fetching cached contact details' ) );
+	}
 }
 
 async function setCachedContactDetails( rawData: DomainContactValidationRequest ): Promise< void > {
@@ -72,11 +76,10 @@ function convertSnakeCaseContactDetailsExtraToCamelCase(
 
 const cachedContactDetailsQueryKey = [ 'user-cached-contact-details' ];
 
-export function useCachedContactDetails( {
-	isLoggedOut,
-}: {
-	isLoggedOut?: boolean;
-} ): PossiblyCompleteDomainContactDetails | null {
+export function useCachedContactDetails( { isLoggedOut }: { isLoggedOut?: boolean } ): {
+	contactDetails: PossiblyCompleteDomainContactDetails | null;
+	isError: boolean;
+} {
 	const result = useQuery( {
 		queryKey: cachedContactDetailsQueryKey,
 		queryFn: fetchCachedContactDetails,
@@ -86,7 +89,11 @@ export function useCachedContactDetails( {
 		},
 		refetchOnWindowFocus: false,
 	} );
-	return result.data ?? null;
+
+	return {
+		contactDetails: result.data ?? null,
+		isError: result.isError,
+	};
 }
 
 export function useUpdateCachedContactDetails(): (
diff --git a/client/my-sites/checkout/src/hooks/use-prefill-checkout-contact-form.ts b/client/my-sites/checkout/src/hooks/use-prefill-checkout-contact-form.ts
index a05278a87509..887dc6fe9b00 100644
--- a/client/my-sites/checkout/src/hooks/use-prefill-checkout-contact-form.ts
+++ b/client/my-sites/checkout/src/hooks/use-prefill-checkout-contact-form.ts
@@ -128,10 +128,24 @@ export function usePrefillCheckoutContactForm( {
 }: {
 	setShouldShowContactDetailsValidationErrors?: ( allowed: boolean ) => void;
 	isLoggedOut?: boolean;
-} ): void {
-	const cachedContactDetails = useCachedContactDetails( { isLoggedOut } );
-	useCachedContactDetailsForCheckoutForm(
-		cachedContactDetails,
+} ): boolean {
+	const { contactDetails, isError } = useCachedContactDetails( { isLoggedOut } );
+
+	const hasCompleted = useCachedContactDetailsForCheckoutForm(
+		contactDetails,
 		setShouldShowContactDetailsValidationErrors
 	);
+
+	// If there is an error, we return it as true and let the form handle it.
+	if ( isError ) {
+		return true;
+	}
+
+	// We don't retrieve cached contact details if the user is logged out.
+	// Unless they already live in the checkout data store.
+	if ( isLoggedOut ) {
+		return true;
+	}
+
+	return hasCompleted;
 }
diff --git a/client/my-sites/checkout/src/payment-methods/pix.tsx b/client/my-sites/checkout/src/payment-methods/pix.tsx
index 058a756624e6..82240410eb64 100644
--- a/client/my-sites/checkout/src/payment-methods/pix.tsx
+++ b/client/my-sites/checkout/src/payment-methods/pix.tsx
@@ -97,7 +97,8 @@ function usePrefillState( state: PixPaymentMethodState ): void {
 	const cartKey = useCartKey();
 	const { responseCart } = useShoppingCart( cartKey );
 	const isLoggedOut = responseCart.cart_key === 'no-user';
-	const contactDetails = useCachedContactDetails( { isLoggedOut } );
+	const { contactDetails } = useCachedContactDetails( { isLoggedOut } );
+
 	// Don't try to pre-fill if the form has been edited. (Also prevents
 	// infinite loops.)
 	if ( state.isTouched ) {
diff --git a/client/my-sites/checkout/src/test/checkout-contact-autocomplete.tsx b/client/my-sites/checkout/src/test/checkout-contact-autocomplete.tsx
index 90969b6832af..831c2c49b70e 100644
--- a/client/my-sites/checkout/src/test/checkout-contact-autocomplete.tsx
+++ b/client/my-sites/checkout/src/test/checkout-contact-autocomplete.tsx
@@ -75,6 +75,7 @@ describe( 'Checkout contact step', () => {
 	} );
 
 	it( 'does not complete the contact step when the contact step button has not been clicked and there are no cached details', async () => {
+		mockCachedContactDetailsEndpoint( {} );
 		const cartChanges = { products: [ planWithoutDomain ] };
 		render( <MockCheckout { ...defaultPropsForMockCheckout } cartChanges={ cartChanges } />, {
 			legacyRoot: true,
diff --git a/client/my-sites/checkout/src/test/checkout-contact-step.tsx b/client/my-sites/checkout/src/test/checkout-contact-step.tsx
index 49ce719f98a8..cf0a025410cc 100644
--- a/client/my-sites/checkout/src/test/checkout-contact-step.tsx
+++ b/client/my-sites/checkout/src/test/checkout-contact-step.tsx
@@ -21,6 +21,7 @@ import {
 	mockMatchMediaOnWindow,
 	mockGetVatInfoEndpoint,
 	countryList,
+	mockCachedContactDetailsEndpoint,
 	mockGetPaymentMethodsEndpoint,
 	mockLogStashEndpoint,
 	mockGetSupportedCountriesEndpoint,
@@ -63,6 +64,8 @@ describe( 'Checkout contact step', () => {
 		mockGetPaymentMethodsEndpoint( [] );
 		mockLogStashEndpoint();
 		mockGetSupportedCountriesEndpoint( countryList );
+		mockCachedContactDetailsEndpoint( {} );
+		mockGetVatInfoEndpoint( {} );
 	} );
 
 	it( 'does render the contact step when the purchase is free and some items are recurring', async () => {
diff --git a/client/my-sites/checkout/src/test/checkout-extra-tax-fields.tsx b/client/my-sites/checkout/src/test/checkout-extra-tax-fields.tsx
index acd6002bd8ad..39d486ed68fb 100644
--- a/client/my-sites/checkout/src/test/checkout-extra-tax-fields.tsx
+++ b/client/my-sites/checkout/src/test/checkout-extra-tax-fields.tsx
@@ -17,6 +17,7 @@ import {
 	planWithBundledDomain,
 	planWithoutDomain,
 	mockSetCartEndpointWith,
+	mockCachedContactDetailsEndpoint,
 	getActivePersonalPlanDataForType,
 	mockContactDetailsValidationEndpoint,
 	getBasicCart,
@@ -82,6 +83,8 @@ describe( 'Checkout contact step extra tax fields', () => {
 		mockGetSupportedCountriesEndpoint( countryList );
 		mockGetVatInfoEndpoint( {} );
 		mockSetCachedContactDetailsEndpoint();
+		mockCachedContactDetailsEndpoint( {} );
+		mockGetVatInfoEndpoint( {} );
 	} );
 
 	it.each( [
diff --git a/client/my-sites/checkout/src/test/checkout-vat-form.tsx b/client/my-sites/checkout/src/test/checkout-vat-form.tsx
index ad8cb1934df9..4136b33e79a6 100644
--- a/client/my-sites/checkout/src/test/checkout-vat-form.tsx
+++ b/client/my-sites/checkout/src/test/checkout-vat-form.tsx
@@ -79,6 +79,8 @@ describe( 'Checkout contact step VAT form', () => {
 		mockMatchMediaOnWindow();
 		mockGetVatInfoEndpoint( {} );
 		mockSetCachedContactDetailsEndpoint();
+		mockCachedContactDetailsEndpoint( {} );
+		mockGetVatInfoEndpoint( {} );
 	} );
 
 	it( 'does not render the VAT field checkbox if the selected country does not support VAT', async () => {
@@ -359,6 +361,7 @@ describe( 'Checkout contact step VAT form', () => {
 			];
 		} );
 		mockContactDetailsValidationEndpoint( 'tax', { success: true } );
+		mockSetVatInfoEndpoint();
 		const user = userEvent.setup();
 		const cartChanges = { products: [ planWithoutDomain ] };
 		render(
diff --git a/client/my-sites/checkout/src/test/use-prefill-checkout-contact-form.tsx b/client/my-sites/checkout/src/test/use-prefill-checkout-contact-form.tsx
index 34e3933a2223..2e70cbfa8636 100644
--- a/client/my-sites/checkout/src/test/use-prefill-checkout-contact-form.tsx
+++ b/client/my-sites/checkout/src/test/use-prefill-checkout-contact-form.tsx
@@ -175,4 +175,17 @@ describe( 'usePrefillCheckoutContactForm', () => {
 		await expect( screen.findByText( 'Form Country: US' ) ).toNeverAppear();
 		await expect( screen.findByText( 'Form Postal: 10001' ) ).toNeverAppear();
 	} );
+
+	it( 'returns true if there is an error with the query', async () => {
+		mockGetSupportedCountriesEndpoint( countryList );
+		mockCachedContactDetailsEndpoint( {}, 500 ); // Simulating API failure
+
+		const reduxStore = createTestReduxStore();
+		const queryClient = new QueryClient();
+		render( <MyTestWrapper reduxStore={ reduxStore } queryClient={ queryClient } /> );
+
+		await waitFor( () => {
+			expect( screen.queryByTestId( 'contact-form--visible' ) ).toBeInTheDocument();
+		} );
+	} );
 } );
diff --git a/client/my-sites/checkout/src/test/util/index.ts b/client/my-sites/checkout/src/test/util/index.ts
index 7b8bb462c0bd..ddb85a9d7c30 100644
--- a/client/my-sites/checkout/src/test/util/index.ts
+++ b/client/my-sites/checkout/src/test/util/index.ts
@@ -1948,10 +1948,10 @@ export const expectedCreateAccountRequest = {
 	},
 };
 
-export function mockCachedContactDetailsEndpoint( responseData ): void {
+export function mockCachedContactDetailsEndpoint( responseData, responseCode = 200 ): void {
 	const endpoint = jest.fn();
 	endpoint.mockReturnValue( true );
-	const mockDomainContactResponse = () => [ 200, responseData ];
+	const mockDomainContactResponse = () => [ responseCode, responseData ];
 	nock( 'https://public-api.wordpress.com' )
 		.get( '/rest/v1.1/me/domain-contact-information' )
 		.reply( mockDomainContactResponse );


Reviews:
user: login: sirbrillig
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-04T21:11:39Z
user: login: sirbrillig
body: This looks good to me! I tested three cases: 

1. With valid cached details.
2. With no cached contact details.
3. With cached but invalid contact details.

All cases worked as expected, showing the new loading text briefly before rendering the step.

The only thing I suggest is that the font styling of the loading message seems quite different from the other text in checkout, or at least the text nearby. Could that be made a little more similar? Anyway, it's not a blocker.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-02-12T17:51:36Z
user: login: chriskmnds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T17:17:02Z
user: login: d-alleyne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:34:42Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T23:18:06Z
user: login: chriskmnds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T05:44:36Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T05:49:39Z
user: login: chriskmnds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T06:06:20Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T06:34:51Z
user: login: chriskmnds
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:34:20Z

Review comments:
diff_hunk: @@ -39,6 +39,9 @@ jest.mock( 'calypso/my-sites/checkout/use-cart-key' );
 jest.mock( 'calypso/lib/analytics/utils/refresh-country-code-cookie-gdpr' );
 jest.mock( 'calypso/state/products-list/selectors/is-marketplace-product' );
 jest.mock( 'calypso/lib/navigate' );
+jest.mock( '../hooks/use-prefill-checkout-contact-form', () => ( {
path: client/my-sites/checkout/src/test/checkout-contact-autocomplete.tsx
user: login: sirbrillig
body: I think rather than mocking the hook's behavior, you can just let the hook do its normal job and rely on mocking the endpoint that returns the cached contact details (and the countries list) like we do here:

https://github.com/Automattic/wp-calypso/blob/7f9b167b40fcbf03688c020dc3eb4204f16f3af4/client/my-sites/checkout/src/test/use-prefill-checkout-contact-form.tsx#L133-L137

As long as your expectations await or use `findBy...` methods, then the contact form should show when the data has arrived and the form is populated.

In fact, we already do that in some of these tests like here:

https://github.com/Automattic/wp-calypso/blob/7f9b167b40fcbf03688c020dc3eb4204f16f3af4/client/my-sites/checkout/src/test/checkout-vat-form.tsx#L167-L180

If I do the following, then the first VAT test passes (I don't know if all this is required; I just copied and pasted from below):

```diff
--- a/client/my-sites/checkout/src/test/checkout-vat-form.tsx
+++ b/client/my-sites/checkout/src/test/checkout-vat-form.tsx
@@ -40,10 +40,6 @@ jest.mock( 'calypso/my-sites/checkout/use-cart-key' );
 jest.mock( 'calypso/lib/analytics/utils/refresh-country-code-cookie-gdpr' );
 jest.mock( 'calypso/state/products-list/selectors/is-marketplace-product' );
 jest.mock( 'calypso/lib/navigate' );
-jest.mock( '../hooks/use-prefill-checkout-contact-form', () => ( {
-       ...jest.requireActual( '../hooks/use-prefill-checkout-contact-form' ), // Spread the real module to keep other functions intact
-       usePrefillCheckoutContactForm: jest.fn().mockReturnValue( true ),
-} ) );

 // These tests seem to be particularly slow (it might be because of using
 // it.each; it's not clear but the timeout might apply to the whole loop
@@ -86,6 +82,21 @@ describe( 'Checkout contact step VAT form', () => {
        } );

        it( 'does not render the VAT field checkbox if the selected country does not support VAT', async () => {
+               nock.cleanAll();
+               mockGetSupportedCountriesEndpoint( countryList );
+               mockGetPaymentMethodsEndpoint( [] );
+               mockCachedContactDetailsEndpoint( {
+                       country_code: 'GB',
+                       postal_code: '',
+               } );
+               mockContactDetailsValidationEndpoint( 'tax', { success: false, messages: [ 'Invalid' ] } );
+               mockGetVatInfoEndpoint( {
+                       id: '12345',
+                       name: 'Test company',
+                       address: '123 Main Street',
+                       country: 'GB',
+               } );
+
                const user = userEvent.setup();
                const cartChanges = { products: [ planWithoutDomain ] };
                render( <MockCheckout { ...defaultPropsForMockCheckout } cartChanges={ cartChanges } />, {
```

The above technique should also allow you to write a test to prove that this change works.
created_at: 2025-02-04T21:08:28Z
author_association: MEMBER
diff_hunk: @@ -1948,10 +1948,10 @@ export const expectedCreateAccountRequest = {
 	},
 };
 
-export function mockCachedContactDetailsEndpoint( responseData ): void {
+export function mockCachedContactDetailsEndpoint( responseData, responseCode = 200 ): void {
path: client/my-sites/checkout/src/test/util/index.ts
user: login: sirbrillig
body: Nice improvement!
created_at: 2025-02-12T17:41:19Z
author_association: MEMBER
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: chriskmnds
body: Weird. I don't see this in GlotPress. Not sure why string freeze hasn't picked this up. @Automattic/i18n any ideas?
created_at: 2025-03-05T17:17:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: d-alleyne
body: It is now visible in GlotPress
created_at: 2025-03-05T18:34:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: StevenDufresne
body: @Automattic/i18n 

I believe the same issue happened in this PR: https://github.com/Automattic/wp-calypso/pull/99325
created_at: 2025-03-05T23:18:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: chriskmnds
body: Yep, I think you just need to push changes/rebase, then also readd the string freeze label. It should refresh it and get the originals back into GlotPress. The problem is they're marked as obsolete after 2 weeks I think, hence also the case in https://github.com/Automattic/wp-calypso/pull/99325

I think we'll be updating some of the messaging in upcoming refactors
created_at: 2025-03-06T05:44:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: StevenDufresne
body: @chriskmnds Why would it say the translations are complete?

<img width="874" alt="Screenshot 2025-03-06 at 2 48 15 PM" src="https://github.com/user-attachments/assets/c2187dbd-77af-4614-a70e-273d2f030475" />

created_at: 2025-03-06T05:49:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: chriskmnds
body: They are complete now. I checked in GP and it's been translated. The problem is it gets marked as obsolete if in a PR for +2 weeks. Refreshing and readding the labels pulls it back
created_at: 2025-03-06T06:06:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: StevenDufresne
body: Ah, if the **PR** is +2 weeks old. Understood. I would expect the system to care about when I added the String Freeze label. The age of the PR is irrelevant.
created_at: 2025-03-06T06:34:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,26 +75,31 @@ export default function WPContactForm( {
 	isLoggedOutCart: boolean;
 	setShouldShowContactDetailsValidationErrors: ( allowed: boolean ) => void;
 } ) {
+	const translate = useTranslate();
 	const contactInfo = useSelect( ( select ) => select( CHECKOUT_STORE ).getContactInfo(), [] );
 	const { formStatus } = useFormStatus();
 	const isStepActive = useIsStepActive();
 	const isDisabled = ! isStepActive || formStatus !== FormStatus.READY;
 
-	usePrefillCheckoutContactForm( {
+	const hasCompleted = usePrefillCheckoutContactForm( {
 		setShouldShowContactDetailsValidationErrors,
 		isLoggedOut: isLoggedOutCart,
 	} );
 
 	return (
-		<BillingFormFields>
-			<ContactDetailsContainer
-				contactDetailsType={ contactDetailsType }
-				contactInfo={ contactInfo }
-				countriesList={ countriesList }
-				shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
-				isDisabled={ isDisabled }
-				isLoggedOutCart={ isLoggedOutCart }
-			/>
+		<BillingFormFields isLoading={ ! hasCompleted }>
+			{ hasCompleted ? (
+				<ContactDetailsContainer
+					contactDetailsType={ contactDetailsType }
+					contactInfo={ contactInfo }
+					countriesList={ countriesList }
+					shouldShowContactDetailsValidationErrors={ shouldShowContactDetailsValidationErrors }
+					isDisabled={ isDisabled }
+					isLoggedOutCart={ isLoggedOutCart }
+				/>
+			) : (
+				<LoadingText>{ translate( 'Retrieving contact details…' ) }</LoadingText>
path: client/my-sites/checkout/src/components/wp-contact-form.tsx
user: login: chriskmnds
body: > Ah, if the PR is +2 weeks old.

I'm not sure if that's the case. I think it's as of when the string freeze PR's been translated or added for translation, like you expect. Not sure what the problem was with https://github.com/Automattic/wp-calypso/pull/99325 (I didn't check when the label was added before commenting) :-)
created_at: 2025-03-06T09:34:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-01-31T06:33:10Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-136083">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-136083%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-136083">https://calypso.live?image=registry.a8c.com/calypso/app:build-136083</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-136083&env=jetpack">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-136083%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-136083&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-136083&env=jetpack</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Automattic for Agencies live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-136083&env=a8c-for-agencies">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-136083%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-136083&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-136083&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: NONE
user: login: matticbot
created_at: 2025-01-31T06:33:44Z
body: <!-- icfy-watermark: c52822 -->
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

**Sections** (~179 bytes added 📈 [gzipped])
<details>

```
name            parsed_size           gzip_size
checkout             +425 B  (+0.0%)     +136 B  (+0.0%)
site-purchases       +135 B  (+0.0%)      +43 B  (+0.0%)
purchases            +135 B  (+0.0%)      +43 B  (+0.0%)
```

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.
</details>

**Async-loaded Components** (~136 bytes added 📈 [gzipped])
<details>

```
name                                             parsed_size           gzip_size
async-load-calypso-blocks-editor-checkout-modal       +425 B  (+0.0%)     +136 B  (+0.0%)
```

React components that are loaded lazily, when a certain part of UI is displayed for the first time.
</details>

**Legend**
<details>
<summary>What is parsed and gzip size?</summary>

**Parsed Size:** Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
**Gzip Size:** Compressed size of the JS and CSS files. This much data needs to be downloaded over network.
</details>

Generated by performance advisor bot at [iscalypsofastyet.com](http://iscalypsofastyet.com).
author_association: CONTRIBUTOR
user: login: StevenDufresne
created_at: 2025-02-03T06:56:36Z
body: @sirbrillig What do you think about this approach? 

Additionally, I was able to fix many of the tests by mocking `hooks/use-prefill-checkout-contact-form` but some of the vat/tax form ones still fail.

I didn't have much success figuring out why but it appears to be something wrong with the mock. You may have more experience here, so wondering if you can think of anything relevant.
author_association: CONTRIBUTOR
user: login: sirbrillig
created_at: 2025-02-03T23:30:31Z
body: I'll take a look tomorrow. 👍 
author_association: MEMBER
user: login: StevenDufresne
created_at: 2025-02-04T07:29:43Z
body: Thanks! 

Looking at the broken tests, mocking `hooks/use-prefill-checkout-contact-form` does indeed work, but tests that rely on state updates initiated in `useCachedContactDetailsForCheckoutForm` do not.  Essentially any test that uses the cachedContact details. I tried unmocking for those tests but it didn't work. I assume we can split those tests out into their own test file, not mock `hooks/use-prefill-checkout-contact-form` and that should work. I didn't have a chance to test that assumption today.

The alternative would be to update all the tests that don't properly wait for `usePrefillCheckoutContactForm` to return. Seems like a heavy lift. 

author_association: CONTRIBUTOR
user: login: sirbrillig
created_at: 2025-02-11T15:50:55Z
body: Looking good! Let me know when you're ready for a final review.
author_association: MEMBER
user: login: StevenDufresne
created_at: 2025-02-12T05:24:14Z
body: Yep, will do! I'm working through some manual testing right now as both a learning exercise for this code and to make sure there's nothing missed by the pre-existing test suites.
author_association: CONTRIBUTOR
user: login: StevenDufresne
created_at: 2025-02-12T07:24:09Z
body: Alright, I've tested more scenarios and added error handling seeing that the hook can block rendering now. 

It was a bit of a journey around the sun learning how some of these pieces fit together. Let me know if there are more logical ways to do this. 
author_association: CONTRIBUTOR
user: login: StevenDufresne
created_at: 2025-02-13T01:03:55Z
body: Thanks for the review!

I'll make a small change exporting `contactDetails` instead of `data` from the hook and improve the text as you mention before merging.
author_association: CONTRIBUTOR
user: login: StevenDufresne
created_at: 2025-02-13T01:46:53Z
body: I've updated the text to have the same color and font-size as the content in other components.

| Default | With Domain |
|--------|--------|
| <img width="684" alt="Screenshot 2025-02-13 at 10 44 33 AM" src="https://github.com/user-attachments/assets/c3d92bda-0dc8-4db1-9ee3-bfbe2b09737f" /> | <img width="681" alt="Screenshot 2025-02-13 at 10 45 06 AM" src="https://github.com/user-attachments/assets/981bfd16-7fb0-4028-ab88-d340630755d6" /> | 



author_association: CONTRIBUTOR
user: login: a8ci18n
created_at: 2025-02-13T04:27:34Z
body: This Pull Request is now available for translation here: https://translate.wordpress.com/deliverables/17249535

Some locales (Hebrew, Japanese) have been temporarily machine-translated due to translator availability. All other translations are usually ready within a few days. Untranslated and machine-translated strings will be sent for translation next Monday and are expected to be completed by the following Friday.

Thank you @StevenDufresne for including a screenshot in the description! This is really helpful for our translators.
author_association: NONE
user: login: a8ci18n
created_at: 2025-02-17T03:30:11Z
body: Translation for this Pull Request [has now been finished](https://translate.wordpress.com/deliverables/overview/17249535).
author_association: NONE
user: login: michaeldcain
created_at: 2025-03-04T22:13:50Z
body: I'm not sure what happened here with the translations. I see "This deliverable has no project associated with it yet" and [don't see any translations for the new string](https://translate.wordpress.com/projects/wpcom/es/default?filters%5Bterm%5D=Retrieving+contact+details&filters%5Bterm_scope%5D=scope_any&filters%5Bstatus%5D=current_or_waiting_or_fuzzy_or_untranslated&filters%5Buser_login%5D=&filters%5Bview%5D=&filter=Filter&sort%5Bby%5D=priority&sort%5Bhow%5D=desc). Pinging @Automattic/i18n to see if they can take a look.
author_association: MEMBER
user: login: StevenDufresne
created_at: 2025-03-07T01:00:51Z
body: Oof. Sorry ya'll. This does not require reviews. 🤦 
author_association: CONTRIBUTOR
user: login: matticbot
created_at: 2025-03-07T01:14:25Z
body: **This PR modifies the release build for the following Calypso Apps:**

_For info about this notification, see here: PCYsg-OT6-p2_

* notifications
* wpcom-block-editor

To test WordPress.com changes, run `install-plugin.sh $pluginSlug try/update-billing-section-loading` on your sandbox.
<!--calypso-app-artifacts-watermark:apr@v1-->
author_association: CONTRIBUTOR