Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useDomainSearchEscapeHatch } from '@automattic/domain-search';
import {
isAIBuilderFlow,
isCopySiteFlow,
Expand All @@ -18,6 +19,7 @@ import { useSelector } from 'react-redux';
import { WPCOMDomainSearch } from 'calypso/components/domains/wpcom-domain-search';
import { FreeDomainForAYearPromo } from 'calypso/components/domains/wpcom-domain-search/free-domain-for-a-year-promo';
import { useQueryHandler } from 'calypso/components/domains/wpcom-domain-search/use-query-handler';
import { useWPCOMDomainSearchEvents } from 'calypso/components/domains/wpcom-domain-search/use-wpcom-domain-search-events';
import FormattedHeader from 'calypso/components/formatted-header';
import { isRelativeUrl } from 'calypso/dashboard/utils/url';
import { SIGNUP_DOMAIN_ORIGIN } from 'calypso/lib/analytics/signup';
Expand Down Expand Up @@ -91,6 +93,8 @@ const DomainSearchStep: StepType< {
currentSiteUrl,
} );

const [ isLoadingExperiment, experimentVariation ] = useDomainSearchEscapeHatch();

const config = useMemo( () => {
const allowedTlds = tldQuery?.split( ',' ) ?? [];

Expand Down Expand Up @@ -124,6 +128,13 @@ const DomainSearchStep: StepType< {
};
}, [ flow, tldQuery, query ] );

const analyticsEvents = useWPCOMDomainSearchEvents( {
vendor: config.vendor,
flowName: flow,
analyticsSection: 'signup',
query: query,
} );

const { submit } = navigation;

const events = useMemo( () => {
Expand Down Expand Up @@ -359,14 +370,30 @@ const DomainSearchStep: StepType< {
};

const getTopBarRightElement = () => {
if ( ! query || ! config.allowsUsingOwnDomain ) {
if ( ! query ) {
return;
}

return (
<Step.LinkButton onClick={ () => events.onExternalDomainClick( query ) }>
{ __( 'Use a domain I already own' ) }
</Step.LinkButton>
<>
{ config.allowsUsingOwnDomain && (
<Step.LinkButton onClick={ () => events.onExternalDomainClick( query ) }>
{ __( 'Use a domain I already own' ) }
</Step.LinkButton>
) }

{ ! isLoadingExperiment &&
experimentVariation === 'treatment_paid_domain_area_free_emphasis_extra_cta' && (
<Step.LinkButton
onClick={ () => {
analyticsEvents.onSkip?.();
events.onSkip();
} }
>
{ __( 'Skip this step' ) }
</Step.LinkButton>
) }
</>
);
};

Expand Down
7 changes: 5 additions & 2 deletions client/lib/explat/internals/anon-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ declare const window: undefined | ( Window & typeof globalThis );
* @param f The callback function
* @param intervalMs The interval in milliseconds
*/
const immediateStartSetInterval = ( f: () => void, intervalMs: number ) => {
const immediateStartSetInterval = (
f: () => void,
intervalMs: number
): ReturnType< typeof setInterval > => {
f();
return setInterval( f, intervalMs );
};
Expand Down Expand Up @@ -47,7 +50,7 @@ export const initializeAnonId = async (): Promise< string | null > => {

let attempt = 0;
initializeAnonIdPromise = new Promise( ( res ) => {
let anonIdPollingInterval: NodeJS.Timeout;
let anonIdPollingInterval: ReturnType< typeof setInterval >;
// eslint-disable-next-line prefer-const
anonIdPollingInterval = immediateStartSetInterval( () => {
const anonId = getTracksAnonymousUserId();
Expand Down
8 changes: 7 additions & 1 deletion client/lib/explat/internals/misc.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export const isDevelopmentMode = process.env.NODE_ENV === 'development';
declare const process: {
env?: {
NODE_ENV?: string;
};
};

export const isDevelopmentMode = process.env?.NODE_ENV === 'development';
15 changes: 15 additions & 0 deletions client/server/lib/logger/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
declare module 'calypso/server/lib/logger' {
type LoggerMethod = ( ...args: Array< unknown > ) => void;

export interface Logger {
trace: LoggerMethod;
debug: LoggerMethod;
info: LoggerMethod;
warn: LoggerMethod;
error: LoggerMethod;
fatal: LoggerMethod;
child( options?: Record< string, unknown > ): Logger;
}

export function getLogger(): Logger;
}
2 changes: 2 additions & 0 deletions packages/domain-search/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"dependencies": {
"@automattic/api-core": "workspace:^",
"@automattic/calypso-products": "workspace:^",
"@automattic/explat-client": "workspace:^",
"@automattic/explat-client-react-helpers": "workspace:^",
"@automattic/i18n-utils": "workspace:^",
"@automattic/urls": "workspace:^",
"@tanstack/react-query": "^5.83.0",
Expand Down
22 changes: 22 additions & 0 deletions packages/domain-search/src/hooks/use-escape-hatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { isOnboardingFlow } from '@automattic/onboarding';
import { useMemo } from 'react';
import { getFlowFromURL } from 'calypso/landing/stepper/utils/get-flow-from-url'; // eslint-disable-line no-restricted-imports
import { useExperiment } from 'calypso/lib/explat'; // eslint-disable-line no-restricted-imports
Copy link
Contributor

Choose a reason for hiding this comment

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

I see you had to import the Explat code from Calypso here. I'm not sure if this might have implications for the build process, but everything seems to be working fine, at least in my tests 🙂

Copy link
Member Author

@m1r0 m1r0 Oct 31, 2025

Choose a reason for hiding this comment

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

As far as I know, this rule is to avoid circular dependencies. Because Explat is so light, I think this won't be an issue, and we'll clean it up after the experiment is done.


export const EXPERIMENT_NAME =
'calypso_signup_onboarding_domain_search_results_page_escape_hatch_202510';

/**
* Hook for the domain search escape hatch experiment.
*/
export const useDomainSearchEscapeHatch = () => {
const flow = useMemo( () => getFlowFromURL(), [] );

const [ isLoading, experimentAssignment ] = useExperiment( EXPERIMENT_NAME, {
isEligible: isOnboardingFlow( flow ),
} );

const variationName = experimentAssignment?.variationName ?? 'control';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it a valid behaviour to load control if experimentAssignment is not set? There is no null or undefined state when the user or flow is not eligible for the experiment?

Copy link
Member Author

Choose a reason for hiding this comment

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

I've seen this in another experiment and thought it made sense to have a default. I'm not sure how this makes much difference for the code ahead in this case. Are you suggesting that this is not needed?


return [ isLoading, variationName ];
};
1 change: 1 addition & 0 deletions packages/domain-search/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './helpers';

export { useDomainSuggestionContainer } from './hooks/use-domain-suggestion-container';
export { useTypedPlaceholder } from './hooks/use-typed-placeholder';
export { useDomainSearchEscapeHatch } from './hooks/use-escape-hatch';

export * from './page';
54 changes: 47 additions & 7 deletions packages/domain-search/src/page/results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,77 @@ import { SearchNotice } from '../components/search-notice';
import { SearchResults } from '../components/search-results';
import { SkipSuggestion } from '../components/skip-suggestion';
import { UnavailableSearchResult } from '../components/unavailable-search-result';
import { useDomainSearchEscapeHatch } from '../hooks/use-escape-hatch';
import { useRequestTracking } from '../hooks/use-request-tracking';
import { useSuggestionsList } from '../hooks/use-suggestions-list';
import { useDomainSearch } from './context';

export const ResultsPage = () => {
const { slots, config } = useDomainSearch();

const { isLoading, featuredSuggestions, regularSuggestions } = useSuggestionsList();
const {
isLoading: isLoadingSuggestions,
featuredSuggestions,
regularSuggestions,
} = useSuggestionsList();
const numberOfInitialVisibleSuggestions =
config.numberOfDomainsResultsPerPage - featuredSuggestions.length;
const [ isLoadingExperiment, experimentVariation ] = useDomainSearchEscapeHatch();
const isLoadingSuggestionsOrExperiment = isLoadingSuggestions || isLoadingExperiment;

const showSkipSuggestionsAfterSearchBar =
! isLoadingExperiment && experimentVariation === 'treatment_above_paid_domain_area';
const showSkipSuggestionsBeforeFeaturedResults =
! isLoadingExperiment &&
[
'treatment_paid_domain_area_skip_emphasis',
'treatment_paid_domain_area_free_emphasis',
'treatment_paid_domain_area_free_emphasis_extra_cta',
'treatment_paid_domain_area',
].includes( experimentVariation as string );
const showSkipSuggestionsAfterFeaturedResults =
! showSkipSuggestionsAfterSearchBar && ! showSkipSuggestionsBeforeFeaturedResults;

useRequestTracking();

return (
<VStack spacing={ 8 } className="domain-search--results">
<VStack spacing={ 4 }>
<SearchBar />
{ ! isLoading && <SearchNotice /> }
{ ! isLoadingSuggestions && <SearchNotice /> }
</VStack>
{ config.skippable && showSkipSuggestionsAfterSearchBar && (
<>
{ isLoadingSuggestionsOrExperiment ? <SkipSuggestion.Placeholder /> : <SkipSuggestion /> }
</>
) }
{ slots?.BeforeResults && <slots.BeforeResults /> }
<VStack spacing={ 4 }>
{ ! isLoading && <UnavailableSearchResult /> }
{ isLoading ? (
{ config.skippable && showSkipSuggestionsBeforeFeaturedResults && (
<>
{ isLoadingSuggestionsOrExperiment ? (
<SkipSuggestion.Placeholder />
) : (
<SkipSuggestion />
) }
</>
) }
{ ! isLoadingSuggestions && <UnavailableSearchResult /> }
{ isLoadingSuggestions ? (
<FeaturedSearchResults.Placeholder />
) : (
<FeaturedSearchResults suggestions={ featuredSuggestions } />
) }
{ config.skippable && (
<> { isLoading ? <SkipSuggestion.Placeholder /> : <SkipSuggestion /> } </>
{ config.skippable && showSkipSuggestionsAfterFeaturedResults && (
<>
{ isLoadingSuggestionsOrExperiment ? (
<SkipSuggestion.Placeholder />
) : (
<SkipSuggestion />
) }
</>
) }
{ isLoading ? (
{ isLoadingSuggestions ? (
<SearchResults.Placeholder />
) : (
<SearchResults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { useDomainSearchEscapeHatch } from '../../hooks/use-escape-hatch';
import { DomainSearchSkipSuggestionPlaceholder } from './index.placeholder';
import { DomainSearchSkipSuggestionSkeleton } from './index.skeleton';

Expand All @@ -25,8 +26,11 @@ const DomainSearchSkipSuggestion = ( {
disabled,
isBusy,
}: Props ) => {
const [ isLoadingExperiment, experimentVariation ] = useDomainSearchEscapeHatch();

let title;
let subtitle;
let buttonText = __( 'Skip purchase' );

if ( existingSiteUrl ) {
const [ domain, ...tld ] = existingSiteUrl.split( '.' );
Expand All @@ -49,24 +53,42 @@ const DomainSearchSkipSuggestion = ( {
} else if ( freeSuggestion ) {
const [ domain, ...tld ] = freeSuggestion.split( '.' );

title = __( 'WordPress.com subdomain' );
subtitle = createInterpolateElement(
sprintf(
// translators: %(domain)s is the domain name, %(tld)s is the top-level domain
__( '<domain>%(domain)s<tld>.%(tld)s</tld></domain> is included' ),
if (
! isLoadingExperiment &&
experimentVariation === 'treatment_paid_domain_area_skip_emphasis'
) {
title = __( 'Prefer to skip for now?' );
buttonText = __( 'Skip this step' );
} else if (
! isLoadingExperiment &&
[
'treatment_paid_domain_area_free_emphasis',
'treatment_paid_domain_area_free_emphasis_extra_cta',
].includes( experimentVariation as string )
) {
title = __( 'Start free with a WordPress.com subdomain' );
subtitle = __( 'Upgrade to a custom domain name anytime.' );
buttonText = __( 'Start Free' );
} else {
title = __( 'WordPress.com subdomain' );
subtitle = createInterpolateElement(
sprintf(
// translators: %(domain)s is the domain name, %(tld)s is the top-level domain
__( '<domain>%(domain)s<tld>.%(tld)s</tld></domain> is included' ),
{
domain,
tld: tld.join( '.' ),
}
),
{
domain,
tld: tld.join( '.' ),
domain: <span style={ { wordBreak: 'break-word', hyphens: 'none' } } />,
tld: <strong style={ { whiteSpace: 'nowrap' } } />,
}
),
{
domain: <span style={ { wordBreak: 'break-word', hyphens: 'none' } } />,
tld: <strong style={ { whiteSpace: 'nowrap' } } />,
}
);
);
}
}

if ( ! title || ! subtitle ) {
if ( ! title ) {
return null;
}

Expand All @@ -79,7 +101,7 @@ const DomainSearchSkipSuggestion = ( {
{ title }
</Heading>
}
subtitle={ <Text>{ subtitle }</Text> }
subtitle={ subtitle && <Text>{ subtitle }</Text> }
right={
<Button
className="domain-search-skip-suggestion__btn"
Expand All @@ -91,7 +113,7 @@ const DomainSearchSkipSuggestion = ( {
isBusy={ isBusy && ! disabled }
__next40pxDefaultSize
>
{ __( 'Skip purchase' ) }
{ buttonText }
</Button>
}
/>
Expand Down
4 changes: 3 additions & 1 deletion packages/domain-search/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"references": [
{ "path": "../components" },
{ "path": "../api-core" },
{ "path": "../api-queries" }
{ "path": "../api-queries" },
{ "path": "../explat-client" },
{ "path": "../explat-client-react-helpers" }
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
}

.step-container-v2__top-bar-right-element {
display: flex;
gap: 1.5rem;
margin-left: auto;
font-size: 0.875rem;
line-height: 1;
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,8 @@ __metadata:
"@automattic/calypso-products": "workspace:^"
"@automattic/calypso-storybook": "workspace:^"
"@automattic/calypso-typescript-config": "workspace:^"
"@automattic/explat-client": "workspace:^"
"@automattic/explat-client-react-helpers": "workspace:^"
"@automattic/i18n-utils": "workspace:^"
"@automattic/urls": "workspace:^"
"@storybook/addon-a11y": "npm:^8.6.14"
Expand Down