Merge remote-tracking branch 'origin/trunk' into remove/creative-mail

This commit is contained in:
Dima 2023-10-26 15:28:36 +03:00
commit 3bc1810f68
282 changed files with 2862 additions and 2712 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove dependency on Jetpack from WooCommerce Shipping & Tax onboarding tasks

View File

@ -37,7 +37,7 @@ export const Plugins = ( {
onAbort, onAbort,
onComplete, onComplete,
onError = () => null, onError = () => null,
pluginSlugs = [ 'jetpack', 'woocommerce-services' ], pluginSlugs = [ 'woocommerce-services' ],
onSkip, onSkip,
installText = __( 'Install & enable', 'woocommerce' ), installText = __( 'Install & enable', 'woocommerce' ),
skipText = __( 'No thanks', 'woocommerce' ), skipText = __( 'No thanks', 'woocommerce' ),

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Export WCUser type for consumption in wcadmin

View File

@ -108,6 +108,7 @@ export {
} from './product-categories/types'; } from './product-categories/types';
export { TaxClass } from './tax-classes/types'; export { TaxClass } from './tax-classes/types';
export { ProductTag, Query } from './product-tags/types'; export { ProductTag, Query } from './product-tags/types';
export { WCUser } from './user/types';
/** /**
* Internal dependencies * Internal dependencies

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
[Single Variation] Add missing tracks events #40996

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Fix checkbox not working when checkedValue is provided

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Fix add/update button interfering with 'status' for variations

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update the variation name, by using the wc_get_formatted_variation.

View File

@ -32,7 +32,7 @@ export function Edit( {
return ( return (
<div { ...blockProps }> <div { ...blockProps }>
<Checkbox <Checkbox
value={ Boolean( value ) } value={ value || null }
onChange={ setValue } onChange={ setValue }
label={ label || '' } label={ label || '' }
title={ title } title={ title }

View File

@ -4,6 +4,11 @@
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
import { ToggleControl } from '@wordpress/components'; import { ToggleControl } from '@wordpress/components';
import { useWooBlockProps } from '@woocommerce/block-templates'; import { useWooBlockProps } from '@woocommerce/block-templates';
import { recordEvent } from '@woocommerce/tracks';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityProp, useEntityId } from '@wordpress/core-data';
/** /**
* Internal dependencies * Internal dependencies
@ -12,6 +17,7 @@ import { ToggleBlockAttributes } from './types';
import { sanitizeHTML } from '../../../utils/sanitize-html'; import { sanitizeHTML } from '../../../utils/sanitize-html';
import { ProductEditorBlockEditProps } from '../../../types'; import { ProductEditorBlockEditProps } from '../../../types';
import useProductEntityProp from '../../../hooks/use-product-entity-prop'; import useProductEntityProp from '../../../hooks/use-product-entity-prop';
import { TRACKS_SOURCE } from '../../../constants';
export function Edit( { export function Edit( {
attributes, attributes,
@ -19,6 +25,7 @@ export function Edit( {
}: ProductEditorBlockEditProps< ToggleBlockAttributes > ) { }: ProductEditorBlockEditProps< ToggleBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes ); const blockProps = useWooBlockProps( attributes );
const { const {
_templateBlockId,
label, label,
property, property,
disabled, disabled,
@ -30,6 +37,12 @@ export function Edit( {
postType, postType,
fallbackValue: false, fallbackValue: false,
} ); } );
const productId = useEntityId( 'postType', postType );
const [ parentId ] = useEntityProp< number >(
'postType',
postType,
'parent_id'
);
function isChecked() { function isChecked() {
if ( checkedValue !== undefined ) { if ( checkedValue !== undefined ) {
@ -39,6 +52,11 @@ export function Edit( {
} }
function handleChange( checked: boolean ) { function handleChange( checked: boolean ) {
recordEvent( 'product_toggle_click', {
block_id: _templateBlockId,
source: TRACKS_SOURCE,
product_id: parentId > 0 ? parentId : productId,
} );
if ( checked ) { if ( checked ) {
setValue( checkedValue !== undefined ? checkedValue : checked ); setValue( checkedValue !== undefined ? checkedValue : checked );
} else { } else {

View File

@ -52,7 +52,8 @@ export function usePublish( {
const isBusy = isSaving || isValidating; const isBusy = isSaving || isValidating;
const isPublished = productStatus === 'publish'; const isPublished =
productType === 'product' ? productStatus === 'publish' : true;
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' ); const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
@ -62,17 +63,25 @@ export function usePublish( {
} }
try { try {
if ( productType === 'product' ) {
await validate( { await validate( {
status: 'publish', status: 'publish',
} ); } );
// The publish button click not only change the status of the product // The publish button click not only change the status of the product
// but also save all the pending changes. So even if the status is // but also save all the pending changes. So even if the status is
// publish it's possible to save the product too. // publish it's possible to save the product too.
if ( ! isPublished ) { if ( ! isPublished ) {
await editEntityRecord( 'postType', productType, productId, { await editEntityRecord(
'postType',
productType,
productId,
{
status: 'publish', status: 'publish',
} ); }
);
}
} else {
await validate();
} }
const publishedProduct = await saveEditedEntityRecord< Product >( const publishedProduct = await saveEditedEntityRecord< Product >(

View File

@ -33,7 +33,8 @@ export function PublishButton( {
productStatus, productStatus,
...props, ...props,
onPublishSuccess( savedProduct: Product ) { onPublishSuccess( savedProduct: Product ) {
const isPublished = productStatus === 'publish'; const isPublished =
productType === 'product' ? productStatus === 'publish' : true;
if ( isPublished ) { if ( isPublished ) {
recordProductEvent( 'product_update', savedProduct ); recordProductEvent( 'product_update', savedProduct );

View File

@ -167,6 +167,8 @@ export const VariationsTable = forwardRef<
batchUpdateProductVariations, batchUpdateProductVariations,
invalidateResolution, invalidateResolution,
} = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME ); } = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
const { invalidateResolution: coreInvalidateResolution } =
useDispatch( 'core' );
const { generateProductVariations } = useProductVariationsHelper(); const { generateProductVariations } = useProductVariationsHelper();
@ -265,6 +267,16 @@ export const VariationsTable = forwardRef<
invalidateResolution( 'getProductVariations', [ invalidateResolution( 'getProductVariations', [
requestParams, requestParams,
] ); ] );
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product',
productId,
] );
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product_variation',
variationId,
] );
} ) } )
.finally( () => { .finally( () => {
setIsUpdating( ( prevState ) => ( { setIsUpdating( ( prevState ) => ( {
@ -333,11 +345,24 @@ export const VariationsTable = forwardRef<
delete: values.map( ( { id } ) => id ), delete: values.map( ( { id } ) => id ),
} }
) )
.then( ( response: VariationResponseProps ) => .then( ( response: VariationResponseProps ) => {
invalidateResolution( 'getProductVariations', [ invalidateResolution( 'getProductVariations', [
requestParams, requestParams,
] ).then( () => response ) ] );
) coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product',
productId,
] );
values.forEach( ( { id: variationId } ) => {
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product_variation',
variationId,
] );
} );
return response;
} )
.then( ( response: VariationResponseProps ) => { .then( ( response: VariationResponseProps ) => {
createSuccessNotice( getSnackbarText( response ) ); createSuccessNotice( getSnackbarText( response ) );
onVariationTableChange( 'delete' ); onVariationTableChange( 'delete' );

View File

@ -65,6 +65,8 @@ export function useProductVariationsHelper() {
generateProductVariations: _generateProductVariations, generateProductVariations: _generateProductVariations,
invalidateResolutionForStoreSelector, invalidateResolutionForStoreSelector,
} = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME ); } = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
const { invalidateResolution: coreInvalidateResolution } =
useDispatch( 'core' );
const [ isGenerating, setIsGenerating ] = useState( false ); const [ isGenerating, setIsGenerating ] = useState( false );
@ -75,7 +77,7 @@ export function useProductVariationsHelper() {
) => { ) => {
setIsGenerating( true ); setIsGenerating( true );
const { status: lastStatus } = await resolveSelect( const { status: lastStatus, variations } = await resolveSelect(
'core' 'core'
).getEditedEntityRecord< Product >( ).getEditedEntityRecord< Product >(
'postType', 'postType',
@ -111,6 +113,20 @@ export function useProductVariationsHelper() {
invalidateResolutionForStoreSelector( invalidateResolutionForStoreSelector(
'getProductVariations' 'getProductVariations'
); );
if ( variations && variations.length > 0 ) {
for ( const variationId of variations ) {
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product_variation',
variationId,
] );
}
}
coreInvalidateResolution( 'getEntityRecord', [
'postType',
'product',
productId,
] );
return invalidateResolutionForStoreSelector( return invalidateResolutionForStoreSelector(
'getProductVariationsTotalCount' 'getProductVariationsTotalCount'
); );

View File

@ -18,6 +18,7 @@ const potentialTrackableProductValueKeys = [
'description', 'description',
'manage_stock', 'manage_stock',
'menu_order', 'menu_order',
'note',
'purchase_note', 'purchase_note',
'sale_price', 'sale_price',
'short_description', 'short_description',
@ -43,6 +44,11 @@ export function recordProductEvent(
product_type: type, product_type: type,
}; };
if ( product.parent_id > 0 ) {
product.note = product.description;
delete product.description;
}
for ( const productValueKey of Object.keys( product ) ) { for ( const productValueKey of Object.keys( product ) ) {
if ( potentialTrackableProductValueKeys.includes( productValueKey ) ) { if ( potentialTrackableProductValueKeys.includes( productValueKey ) ) {
const eventPropKey = const eventPropKey =

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Fix Woo AI webpack build configuration.

View File

@ -4,7 +4,7 @@ const WooCommerceDependencyExtractionWebpackPlugin = require( '@woocommerce/depe
module.exports = { module.exports = {
...defaultConfig, ...defaultConfig,
entry: { entry: {
...defaultConfig.entry, index: './src/index.ts',
}, },
module: { module: {
...defaultConfig.module, ...defaultConfig.module,

View File

@ -0,0 +1,23 @@
/**
* WordPress dependencies
*/
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
let consentString =
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.';
if ( window.wcSettings && window.wcSettings.wpVersion ) {
// Parse the version string as a floating-point number
const wpVersion = parseFloat( window.wcSettings.wpVersion );
if ( ! isNaN( wpVersion ) && wpVersion >= 6.4 ) {
consentString =
'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.';
}
}
export const { lock, unlock } =
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
consentString,
'@wordpress/edit-site'
);
//# sourceMappingURL=lock-unlock.js.map

View File

@ -76,12 +76,58 @@ const recordTracksSkipBusinessLocationCompleted = () => {
} ); } );
}; };
// Temporarily expand the step viewed track for BusinessInfo so that we can include the experiment assignment
// Remove this and change the action back to recordTracksStepViewed when the experiment is over
const recordTracksStepViewedBusinessInfo = (
context: CoreProfilerStateMachineContext,
_event: unknown,
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
recordEvent( 'coreprofiler_step_view', {
step,
email_marketing_experiment_assignment:
context.emailMarketingExperimentAssignment,
wc_version: getSetting( 'wcVersion' ),
} );
};
const recordTracksIsEmailChanged = (
context: CoreProfilerStateMachineContext,
event: BusinessInfoEvent
) => {
if ( context.emailMarketingExperimentAssignment === 'treatment' ) {
let emailSource, isEmailChanged;
if ( context.onboardingProfile.store_email ) {
emailSource = 'onboarding_profile_store_email'; // from previous entry
isEmailChanged =
event.payload.storeEmailAddress !==
context.onboardingProfile.store_email;
} else if ( context.currentUserEmail ) {
emailSource = 'current_user_email'; // from currentUser
isEmailChanged =
event.payload.storeEmailAddress !== context.currentUserEmail;
} else {
emailSource = 'was_empty';
isEmailChanged = event.payload.storeEmailAddress?.length > 0;
}
recordEvent( 'coreprofiler_email_marketing', {
opt_in: event.payload.isOptInMarketing,
email_field_prefilled_source: emailSource,
email_field_modified: isEmailChanged,
} );
}
};
const recordTracksBusinessInfoCompleted = ( const recordTracksBusinessInfoCompleted = (
_context: CoreProfilerStateMachineContext, context: CoreProfilerStateMachineContext,
event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } > event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } >
) => { ) => {
recordEvent( 'coreprofiler_step_complete', { recordEvent( 'coreprofiler_step_complete', {
step: 'business_info', step: 'business_info',
email_marketing_experiment_assignment:
context.emailMarketingExperimentAssignment,
wc_version: getSetting( 'wcVersion' ), wc_version: getSetting( 'wcVersion' ),
} ); } );
@ -92,8 +138,8 @@ const recordTracksBusinessInfoCompleted = (
) === -1, ) === -1,
industry: event.payload.industry, industry: event.payload.industry,
store_location_previously_set: store_location_previously_set:
_context.onboardingProfile.is_store_country_set || false, context.onboardingProfile.is_store_country_set || false,
geolocation_success: _context.geolocatedLocation !== undefined, geolocation_success: context.geolocatedLocation !== undefined,
geolocation_overruled: event.payload.geolocationOverruled, geolocation_overruled: event.payload.geolocationOverruled,
} ); } );
}; };
@ -180,4 +226,6 @@ export default {
recordFailedPluginInstallations, recordFailedPluginInstallations,
recordSuccessfulPluginInstallation, recordSuccessfulPluginInstallation,
recordTracksPluginsInstallationRequest, recordTracksPluginsInstallationRequest,
recordTracksIsEmailChanged,
recordTracksStepViewedBusinessInfo,
}; };

View File

@ -31,8 +31,13 @@ import {
GeolocationResponse, GeolocationResponse,
PLUGINS_STORE_NAME, PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME, SETTINGS_STORE_NAME,
USER_STORE_NAME,
WCUser,
} from '@woocommerce/data'; } from '@woocommerce/data';
import { initializeExPlat } from '@woocommerce/explat'; import {
initializeExPlat,
loadExperimentAssignment,
} from '@woocommerce/explat';
import { CountryStateOption } from '@woocommerce/onboarding'; import { CountryStateOption } from '@woocommerce/onboarding';
import { getAdminLink } from '@woocommerce/settings'; import { getAdminLink } from '@woocommerce/settings';
import CurrencyFactory from '@woocommerce/currency'; import CurrencyFactory from '@woocommerce/currency';
@ -99,6 +104,8 @@ export type BusinessInfoEvent = {
industry?: IndustryChoice; industry?: IndustryChoice;
storeLocation: CountryStateOption[ 'key' ]; storeLocation: CountryStateOption[ 'key' ];
geolocationOverruled: boolean; geolocationOverruled: boolean;
isOptInMarketing: boolean;
storeEmailAddress: string;
}; };
}; };
@ -139,6 +146,8 @@ export type OnboardingProfile = {
selling_platforms: SellingPlatform[] | null; selling_platforms: SellingPlatform[] | null;
skip?: boolean; skip?: boolean;
is_store_country_set: boolean | null; is_store_country_set: boolean | null;
store_email?: string;
is_agree_marketing?: boolean;
}; };
export type PluginsPageSkippedEvent = { export type PluginsPageSkippedEvent = {
@ -195,6 +204,8 @@ export type CoreProfilerStateMachineContext = {
persistBusinessInfoRef?: ReturnType< typeof spawn >; persistBusinessInfoRef?: ReturnType< typeof spawn >;
spawnUpdateOnboardingProfileOptionRef?: ReturnType< typeof spawn >; spawnUpdateOnboardingProfileOptionRef?: ReturnType< typeof spawn >;
spawnGeolocationRef?: ReturnType< typeof spawn >; spawnGeolocationRef?: ReturnType< typeof spawn >;
emailMarketingExperimentAssignment: 'treatment' | 'control';
currentUserEmail: string | undefined;
}; };
const getAllowTrackingOption = async () => const getAllowTrackingOption = async () =>
@ -309,6 +320,35 @@ const handleOnboardingProfileOption = assign( {
}, },
} ); } );
const getMarketingOptInExperimentAssignment = async () => {
return loadExperimentAssignment(
`woocommerce_core_profiler_email_marketing_opt_in_2023_Q4_V1`
);
};
const getCurrentUserEmail = async () => {
const currentUser: WCUser< 'email' > = await resolveSelect(
USER_STORE_NAME
).getCurrentUser();
return currentUser?.email;
};
const assignCurrentUserEmail = assign( {
currentUserEmail: (
_context,
event: DoneInvokeEvent< string | undefined >
) => {
if (
event.data &&
event.data.length > 0 &&
event.data !== 'wordpress@example.com' // wordpress default prefilled email address
) {
return event.data;
}
return undefined;
},
} );
const assignOnboardingProfile = assign( { const assignOnboardingProfile = assign( {
onboardingProfile: ( onboardingProfile: (
_context, _context,
@ -316,6 +356,17 @@ const assignOnboardingProfile = assign( {
) => event.data, ) => event.data,
} ); } );
const assignMarketingOptInExperimentAssignment = assign( {
emailMarketingExperimentAssignment: (
_context,
event: DoneInvokeEvent<
Awaited<
ReturnType< typeof getMarketingOptInExperimentAssignment >
>
>
) => event.data.variationName ?? 'control',
} );
const getGeolocation = async ( context: CoreProfilerStateMachineContext ) => { const getGeolocation = async ( context: CoreProfilerStateMachineContext ) => {
if ( context.optInDataSharing ) { if ( context.optInDataSharing ) {
return resolveSelect( COUNTRIES_STORE_NAME ).geolocate(); return resolveSelect( COUNTRIES_STORE_NAME ).geolocate();
@ -499,6 +550,11 @@ const updateBusinessInfo = async (
...refreshedOnboardingProfile, ...refreshedOnboardingProfile,
is_store_country_set: true, is_store_country_set: true,
industry: [ event.payload.industry ], industry: [ event.payload.industry ],
is_agree_marketing: event.payload.isOptInMarketing,
store_email:
event.payload.storeEmailAddress.length > 0
? event.payload.storeEmailAddress
: null,
}, },
} ); } );
}; };
@ -644,6 +700,8 @@ const coreProfilerMachineActions = {
handleCountries, handleCountries,
handleOnboardingProfileOption, handleOnboardingProfileOption,
assignOnboardingProfile, assignOnboardingProfile,
assignMarketingOptInExperimentAssignment,
assignCurrentUserEmail,
persistBusinessInfo, persistBusinessInfo,
spawnUpdateOnboardingProfileOption, spawnUpdateOnboardingProfileOption,
redirectToWooHome, redirectToWooHome,
@ -657,6 +715,8 @@ const coreProfilerMachineServices = {
getCountries, getCountries,
getGeolocation, getGeolocation,
getOnboardingProfileOption, getOnboardingProfileOption,
getMarketingOptInExperimentAssignment,
getCurrentUserEmail,
getPlugins, getPlugins,
browserPopstateHandler, browserPopstateHandler,
updateBusinessInfo, updateBusinessInfo,
@ -693,6 +753,8 @@ export const coreProfilerStateMachineDefinition = createMachine( {
loader: {}, loader: {},
onboardingProfile: {} as OnboardingProfile, onboardingProfile: {} as OnboardingProfile,
jetpackAuthUrl: undefined, jetpackAuthUrl: undefined,
emailMarketingExperimentAssignment: 'control',
currentUserEmail: undefined,
} as CoreProfilerStateMachineContext, } as CoreProfilerStateMachineContext,
states: { states: {
navigate: { navigate: {
@ -1026,6 +1088,45 @@ export const coreProfilerStateMachineDefinition = createMachine( {
}, },
}, },
}, },
marketingOptInExperiment: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'getMarketingOptInExperimentAssignment',
onDone: {
target: 'done',
actions: [
'assignMarketingOptInExperimentAssignment',
],
},
},
},
done: { type: 'final' },
},
},
currentUserEmail: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'getCurrentUserEmail',
onDone: {
target: 'done',
actions: [
'assignCurrentUserEmail',
],
},
onError: {
target: 'done',
},
},
},
done: {
type: 'final',
},
},
},
}, },
// onDone is reached when child parallel states fo fetching are resolved (reached final states) // onDone is reached when child parallel states fo fetching are resolved (reached final states)
onDone: { onDone: {
@ -1039,14 +1140,17 @@ export const coreProfilerStateMachineDefinition = createMachine( {
}, },
entry: [ entry: [
{ {
type: 'recordTracksStepViewed', type: 'recordTracksStepViewedBusinessInfo',
step: 'business_info', step: 'business_info',
}, },
], ],
on: { on: {
BUSINESS_INFO_COMPLETED: { BUSINESS_INFO_COMPLETED: {
target: 'postBusinessInfo', target: 'postBusinessInfo',
actions: [ 'recordTracksBusinessInfoCompleted' ], actions: [
'recordTracksBusinessInfoCompleted',
'recordTracksIsEmailChanged',
],
}, },
}, },
}, },

View File

@ -2,7 +2,13 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Button, TextControl, Notice, Spinner } from '@wordpress/components'; import {
Button,
TextControl,
Notice,
Spinner,
CheckboxControl,
} from '@wordpress/components';
import { SelectControl } from '@woocommerce/components'; import { SelectControl } from '@woocommerce/components';
import { Icon, chevronDown } from '@wordpress/icons'; import { Icon, chevronDown } from '@wordpress/icons';
import { import {
@ -83,9 +89,18 @@ export type BusinessInfoContextProps = Pick<
> & { > & {
onboardingProfile: Pick< onboardingProfile: Pick<
CoreProfilerStateMachineContext[ 'onboardingProfile' ], CoreProfilerStateMachineContext[ 'onboardingProfile' ],
'industry' | 'business_choice' | 'is_store_country_set' | 'industry'
| 'business_choice'
| 'is_store_country_set'
| 'is_agree_marketing'
| 'store_email'
>;
} & Partial<
Pick<
CoreProfilerStateMachineContext,
'emailMarketingExperimentAssignment' | 'currentUserEmail'
>
>; >;
};
export const BusinessInfo = ( { export const BusinessInfo = ( {
context, context,
@ -105,7 +120,11 @@ export const BusinessInfo = ( {
is_store_country_set: isStoreCountrySet, is_store_country_set: isStoreCountrySet,
industry: industryFromOnboardingProfile, industry: industryFromOnboardingProfile,
business_choice: businessChoiceFromOnboardingProfile, business_choice: businessChoiceFromOnboardingProfile,
is_agree_marketing: isOptInMarketingFromOnboardingProfile,
store_email: storeEmailAddressFromOnboardingProfile,
}, },
emailMarketingExperimentAssignment,
currentUserEmail,
} = context; } = context;
const [ storeName, setStoreName ] = useState( const [ storeName, setStoreName ] = useState(
@ -176,6 +195,14 @@ export const BusinessInfo = ( {
const [ hasSubmitted, setHasSubmitted ] = useState( false ); const [ hasSubmitted, setHasSubmitted ] = useState( false );
const [ storeEmailAddress, setEmailAddress ] = useState(
storeEmailAddressFromOnboardingProfile || currentUserEmail || ''
);
const [ isOptInMarketing, setIsOptInMarketing ] = useState< boolean >(
isOptInMarketingFromOnboardingProfile || false
);
return ( return (
<div <div
className="woocommerce-profiler-business-information" className="woocommerce-profiler-business-information"
@ -345,12 +372,55 @@ export const BusinessInfo = ( {
</ul> </ul>
</Notice> </Notice>
) } ) }
{ emailMarketingExperimentAssignment === 'treatment' && (
<>
<TextControl
className="woocommerce-profiler-business-info-email-adddress"
onChange={ ( value ) => {
setEmailAddress( value );
} }
value={ decodeEntities( storeEmailAddress ) }
label={
<>
{ __(
'Your email address',
'woocommerce'
) }
{ isOptInMarketing && (
<span className="woocommerce-profiler-question-required">
{ '*' }
</span>
) }
</>
}
placeholder={ __(
'wordpress@example.com',
'woocommerce'
) }
/>
<CheckboxControl
className="core-profiler__checkbox"
label={ __(
'Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox.',
'woocommerce'
) }
checked={ isOptInMarketing }
onChange={ setIsOptInMarketing }
/>
</>
) }
</form> </form>
<div className="woocommerce-profiler-button-container"> <div className="woocommerce-profiler-button-container">
<Button <Button
className="woocommerce-profiler-button" className="woocommerce-profiler-button"
variant="primary" variant="primary"
disabled={ ! storeCountry.key } disabled={
! storeCountry.key ||
( emailMarketingExperimentAssignment ===
'treatment' &&
isOptInMarketing &&
storeEmailAddress.length === 0 )
}
onClick={ () => { onClick={ () => {
sendEvent( { sendEvent( {
type: 'BUSINESS_INFO_COMPLETED', type: 'BUSINESS_INFO_COMPLETED',
@ -360,6 +430,8 @@ export const BusinessInfo = ( {
storeLocation: storeCountry.key, storeLocation: storeCountry.key,
geolocationOverruled: geolocationOverruled:
geolocationOverruled || false, geolocationOverruled || false,
isOptInMarketing,
storeEmailAddress,
}, },
} ); } );
setHasSubmitted( true ); setHasSubmitted( true );

View File

@ -173,6 +173,8 @@ describe( 'BusinessInfo', () => {
industry: 'other', industry: 'other',
storeLocation: 'AU:VIC', storeLocation: 'AU:VIC',
storeName: '', storeName: '',
isOptInMarketing: false,
storeEmailAddress: '',
}, },
type: 'BUSINESS_INFO_COMPLETED', type: 'BUSINESS_INFO_COMPLETED',
} ); } );
@ -224,6 +226,8 @@ describe( 'BusinessInfo', () => {
industry: 'other', industry: 'other',
storeLocation: 'AW', storeLocation: 'AW',
storeName: '', storeName: '',
isOptInMarketing: false,
storeEmailAddress: '',
}, },
type: 'BUSINESS_INFO_COMPLETED', type: 'BUSINESS_INFO_COMPLETED',
} ); } );
@ -273,6 +277,8 @@ describe( 'BusinessInfo', () => {
industry: 'food_and_drink', industry: 'food_and_drink',
storeLocation: 'AU:VIC', storeLocation: 'AU:VIC',
storeName: 'Test Store Name', storeName: 'Test Store Name',
isOptInMarketing: false,
storeEmailAddress: '',
}, },
type: 'BUSINESS_INFO_COMPLETED', type: 'BUSINESS_INFO_COMPLETED',
} ); } );
@ -301,8 +307,106 @@ describe( 'BusinessInfo', () => {
industry: 'food_and_drink', industry: 'food_and_drink',
storeLocation: 'AU:VIC', storeLocation: 'AU:VIC',
storeName: 'Test Store Name', storeName: 'Test Store Name',
isOptInMarketing: false,
storeEmailAddress: '',
}, },
type: 'BUSINESS_INFO_COMPLETED', type: 'BUSINESS_INFO_COMPLETED',
} ); } );
} ); } );
describe( 'business info page, email marketing variant', () => {
beforeEach( () => {
props.context.emailMarketingExperimentAssignment = 'treatment';
} );
it( 'should correctly render the experiment variant with the email field', () => {
render( <BusinessInfo { ...props } /> );
expect(
screen.getByText( /Your email address/i )
).toBeInTheDocument();
} );
it( 'should not disable the continue field when experiment variant is shown, opt in checkbox is not checked and email field is empty', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
expect( continueButton ).not.toBeDisabled();
} );
it( 'should disable the continue field when experiment variant is shown, opt in checkbox is checked and email field is empty', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const checkbox = screen.getByRole( 'checkbox', {
name: /Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox./i,
} );
userEvent.click( checkbox );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
expect( continueButton ).toBeDisabled();
} );
it( 'should correctly send event with opt-in true when experiment variant is shown, opt in checkbox is checked and email field is filled', () => {
props.context.businessInfo.location = 'AW';
props.context.onboardingProfile.is_store_country_set = true;
render( <BusinessInfo { ...props } /> );
const checkbox = screen.getByRole( 'checkbox', {
name: /Opt-in to receive tips, discounts, and recommendations from the Woo team directly in your inbox./i,
} );
userEvent.click( checkbox );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
userEvent.type( emailInput, 'wordpress@automattic.com' );
const continueButton = screen.getByRole( 'button', {
name: /Continue/i,
} );
userEvent.click( continueButton );
expect( props.sendEvent ).toHaveBeenCalledWith( {
payload: {
geolocationOverruled: false,
industry: 'other',
storeLocation: 'AW',
storeName: '',
isOptInMarketing: true,
storeEmailAddress: 'wordpress@automattic.com',
},
type: 'BUSINESS_INFO_COMPLETED',
} );
} );
it( 'should correctly prepopulate the email field if populated in the onboarding profile', () => {
props.context.onboardingProfile.store_email =
'wordpress@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'wordpress@automattic.com' );
} );
it( 'should correctly prepopulate the email field if populated in the current user', () => {
props.context.currentUserEmail = 'currentUser@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'currentUser@automattic.com' );
} );
it( 'should correctly favor the onboarding profile email over the current user email', () => {
props.context.currentUserEmail = 'currentUser@automattic.com';
props.context.onboardingProfile.store_email =
'wordpress@automattic.com';
render( <BusinessInfo { ...props } /> );
const emailInput = screen.getByRole( 'textbox', {
name: /Your email address/i,
} );
expect( emailInput ).toHaveValue( 'wordpress@automattic.com' );
} );
} );
} ); } );

View File

@ -419,6 +419,8 @@
} }
.woocommerce-profiler-question-label, .woocommerce-profiler-question-label,
.woocommerce-profiler-business-info-email-adddress
.components-base-control__label,
.woocommerce-profiler-business-info-store-name .woocommerce-profiler-business-info-store-name
.components-base-control__label { .components-base-control__label {
text-transform: uppercase; text-transform: uppercase;
@ -430,11 +432,15 @@
} }
.woocommerce-profiler-question-label .woocommerce-profiler-question-label
.woocommerce-profiler-question-required,
.woocommerce-profiler-business-info-email-adddress
.woocommerce-profiler-question-required { .woocommerce-profiler-question-required {
color: #cc1818; color: #cc1818;
padding-left: 3px; padding-left: 3px;
} }
.woocommerce-profiler-business-info-email-adddress
.components-text-control__input,
.woocommerce-profiler-business-info-store-name .woocommerce-profiler-business-info-store-name
.components-text-control__input { .components-text-control__input {
height: 40px; height: 40px;
@ -448,6 +454,29 @@
} }
} }
.woocommerce-profiler-select-control__country-spacer + .woocommerce-profiler-business-info-email-adddress {
margin-top: 8px;
}
.woocommerce-profiler-business-info-email-adddress {
margin-top: 20px;
}
.core-profiler__checkbox {
margin-top: 4px;
.components-checkbox-control__input-container {
margin-right: 16px;
}
.components-checkbox-control__label {
color: $gray-700;
font-size: 12px;
line-height: 16px;
font-weight: 400;
}
}
.woocommerce-profiler-select-control__industry { .woocommerce-profiler-select-control__industry {
margin-bottom: 20px; margin-bottom: 20px;
} }

View File

@ -60,7 +60,7 @@ function ScaledBlockPreview( {
isNavigable = false, isNavigable = false,
isScrollable = true, isScrollable = true,
}: ScaledBlockPreviewProps ) { }: ScaledBlockPreviewProps ) {
const { setLogoBlock } = useContext( LogoBlockContext ); const { setLogoBlockIds } = useContext( LogoBlockContext );
const [ fontFamilies ] = useGlobalSetting( const [ fontFamilies ] = useGlobalSetting(
'typography.fontFamilies.theme' 'typography.fontFamilies.theme'
) as [ FontFamily[] ]; ) as [ FontFamily[] ];
@ -182,18 +182,18 @@ function ScaledBlockPreview( {
// Get the current logo block client ID from DOM and set it in the logo block context. This is used for the logo settings. See: ./sidebar/sidebar-navigation-screen-logo.tsx // Get the current logo block client ID from DOM and set it in the logo block context. This is used for the logo settings. See: ./sidebar/sidebar-navigation-screen-logo.tsx
// Ideally, we should be able to get the logo block client ID from the block editor store but it is not available. // Ideally, we should be able to get the logo block client ID from the block editor store but it is not available.
// We should update this code once the there is a selector in the block editor store that can be used to get the logo block client ID. // We should update this code once the there is a selector in the block editor store that can be used to get the logo block client ID.
const siteLogo = bodyElement.querySelector( const siteLogos = bodyElement.querySelectorAll(
'.wp-block-site-logo' '.wp-block-site-logo'
); );
const blockClientId = siteLogo const logoBlockIds = Array.from( siteLogos )
? siteLogo.getAttribute( 'data-block' ) .map( ( siteLogo ) => {
: null; return siteLogo.getAttribute(
'data-block'
setLogoBlock( { );
clientId: blockClientId, } )
isLoading: false, .filter( Boolean ) as string[];
} ); setLogoBlockIds( logoBlockIds );
if ( isNavigable ) { if ( isNavigable ) {
enableNavigation(); enableNavigation();
@ -221,10 +221,7 @@ function ScaledBlockPreview( {
return () => { return () => {
observer.disconnect(); observer.disconnect();
possiblyRemoveAllListeners(); possiblyRemoveAllListeners();
setLogoBlock( { setLogoBlockIds( [] );
clientId: null,
isLoading: true,
} );
}; };
}, },
[ isNavigable ] [ isNavigable ]
@ -262,13 +259,10 @@ function ScaledBlockPreview( {
` } ` }
</style> </style>
<MemoizedBlockList renderAppender={ false } /> <MemoizedBlockList renderAppender={ false } />
{ /* Only load font families when there are two font families (font-paring selection). Otherwise, it is not needed. */ }
{ externalFontFamilies.length === 2 && (
<FontFamiliesLoader <FontFamiliesLoader
fontFamilies={ externalFontFamilies } fontFamilies={ externalFontFamilies }
onLoad={ noop } onLoad={ noop }
/> />
) }
</Iframe> </Iframe>
</DisabledProvider> </DisabledProvider>
); );

View File

@ -15,7 +15,7 @@ import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
import useSiteEditorSettings from '@wordpress/edit-site/build-module/components/block-editor/use-site-editor-settings'; import useSiteEditorSettings from '@wordpress/edit-site/build-module/components/block-editor/use-site-editor-settings';
import { useQuery } from '@woocommerce/navigation'; import { useQuery } from '@woocommerce/navigation';
import { useContext, useCallback } from '@wordpress/element'; import { useContext, useCallback, useMemo } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
@ -24,6 +24,7 @@ import BlockPreview from './block-preview';
import { useEditorBlocks } from './hooks/use-editor-blocks'; import { useEditorBlocks } from './hooks/use-editor-blocks';
import { useScrollOpacity } from './hooks/use-scroll-opacity'; import { useScrollOpacity } from './hooks/use-scroll-opacity';
import { CustomizeStoreContext } from './'; import { CustomizeStoreContext } from './';
import { HighlightedBlockContext } from './context/highlighted-block-context';
const { useHistory } = unlock( routerPrivateApis ); const { useHistory } = unlock( routerPrivateApis );
@ -118,13 +119,45 @@ export const BlockEditor = ( {} ) => {
[ history, urlParams, pages ] [ history, urlParams, pages ]
); );
const [ , , onChange ] = useEditorBlocks();
const { highlightedBlockIndex } = useContext( HighlightedBlockContext );
const isHighlighting = highlightedBlockIndex !== -1;
const additionalStyles = isHighlighting
? `
.wp-block.preview-opacity {
opacity: ${ previewOpacity };
}
`
: '';
const renderedBlocks = useMemo(
() =>
blocks.map( ( block, i ) => {
if ( ! isHighlighting || i === highlightedBlockIndex ) {
return block;
}
return {
...block,
attributes: {
...block.attributes,
className:
block.attributes.className + ' preview-opacity',
},
};
} ),
[ blocks, highlightedBlockIndex, isHighlighting ]
);
return ( return (
<div className="woocommerce-customize-store__block-editor"> <div className="woocommerce-customize-store__block-editor">
<div className={ 'woocommerce-block-preview-container' }> <div className={ 'woocommerce-block-preview-container' }>
<BlockPreview <BlockPreview
blocks={ blocks } blocks={ renderedBlocks }
onChange={ isHighlighting ? undefined : onChange }
settings={ settings } settings={ settings }
additionalStyles={ '' } additionalStyles={ additionalStyles }
isNavigable={ false } isNavigable={ false }
isScrollable={ currentState !== 'transitionalScreen' } isScrollable={ currentState !== 'transitionalScreen' }
onClickNavigationItem={ onClickNavigationItem } onClickNavigationItem={ onClickNavigationItem }

View File

@ -7,7 +7,7 @@
*/ */
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
import { BlockEditorProvider } from '@wordpress/block-editor'; import { BlockEditorProvider } from '@wordpress/block-editor';
import { memo, useContext, useMemo } from '@wordpress/element'; import { memo, useMemo } from '@wordpress/element';
import { BlockInstance } from '@wordpress/blocks'; import { BlockInstance } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
@ -16,63 +16,38 @@ import {
AutoHeightBlockPreview, AutoHeightBlockPreview,
ScaledBlockPreviewProps, ScaledBlockPreviewProps,
} from './auto-block-preview'; } from './auto-block-preview';
import { HighlightedBlockContext } from './context/highlighted-block-context'; import { ChangeHandler } from './hooks/use-editor-blocks';
import { useEditorBlocks } from './hooks/use-editor-blocks';
export const BlockPreview = ( { export const BlockPreview = ( {
blocks, blocks,
settings, settings,
useSubRegistry = true, useSubRegistry = true,
additionalStyles, additionalStyles,
previewOpacity = 0.5, onChange,
...props ...props
}: { }: {
blocks: BlockInstance | BlockInstance[]; blocks: BlockInstance | BlockInstance[];
settings: Record< string, unknown >; settings: Record< string, unknown >;
onChange?: ChangeHandler | undefined;
useSubRegistry?: boolean; useSubRegistry?: boolean;
previewOpacity?: number; previewOpacity?: number;
} & Omit< ScaledBlockPreviewProps, 'containerWidth' > ) => { } & Omit< ScaledBlockPreviewProps, 'containerWidth' > ) => {
const [ , , onChange ] = useEditorBlocks();
const { highlightedBlockIndex } = useContext( HighlightedBlockContext );
const renderedBlocks = useMemo( () => { const renderedBlocks = useMemo( () => {
const _blocks = Array.isArray( blocks ) ? blocks : [ blocks ]; const _blocks = Array.isArray( blocks ) ? blocks : [ blocks ];
return _blocks;
return _blocks.map( ( block, i ) => { }, [ blocks ] );
if ( i === highlightedBlockIndex ) {
return block;
}
return {
...block,
attributes: {
...block.attributes,
className: block.attributes.className + ' preview-opacity',
},
};
} );
}, [ blocks, highlightedBlockIndex ] );
const opacityStyles =
highlightedBlockIndex === -1
? ''
: `
.wp-block.preview-opacity {
opacity: ${ previewOpacity };
}
`;
return ( return (
<BlockEditorProvider <BlockEditorProvider
value={ renderedBlocks } value={ renderedBlocks }
settings={ settings } settings={ settings }
// We need to set onChange for logo to work, but we don't want to trigger the onChange callback when highlighting blocks in the preview. It would persist the highlighted block and cause the opacity to be applied to block permanently. // We need to set onChange for logo to work, but we don't want to trigger the onChange callback when highlighting blocks in the preview. It would persist the highlighted block and cause the opacity to be applied to block permanently.
onChange={ opacityStyles ? undefined : onChange } onChange={ onChange }
useSubRegistry={ useSubRegistry } useSubRegistry={ useSubRegistry }
> >
<AutoHeightBlockPreview <AutoHeightBlockPreview
settings={ settings } settings={ settings }
additionalStyles={ `${ opacityStyles } ${ additionalStyles }` } additionalStyles={ additionalStyles }
{ ...props } { ...props }
/> />
</BlockEditorProvider> </BlockEditorProvider>

View File

@ -5,7 +5,7 @@
* External dependencies * External dependencies
*/ */
import classnames from 'classnames'; import classnames from 'classnames';
import { useMemo } from '@wordpress/element'; import { useEffect, useMemo } from '@wordpress/element';
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
import { InterfaceSkeleton } from '@wordpress/interface'; import { InterfaceSkeleton } from '@wordpress/interface';
import { useSelect, useDispatch } from '@wordpress/data'; import { useSelect, useDispatch } from '@wordpress/data';
@ -24,6 +24,7 @@ import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/componen
* Internal dependencies * Internal dependencies
*/ */
import { BlockEditor } from './block-editor'; import { BlockEditor } from './block-editor';
import { editorIsLoaded } from '../utils';
export const Editor = ( { isLoading }: { isLoading: boolean } ) => { export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
const { context, hasPageContentFocus } = useSelect( ( select ) => { const { context, hasPageContentFocus } = useSelect( ( select ) => {
@ -59,6 +60,12 @@ export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
}; };
}, [ hasPageContentFocus, context, setEditedPostContext ] ); }, [ hasPageContentFocus, context, setEditedPostContext ] );
useEffect( () => {
if ( ! isLoading ) {
editorIsLoaded();
}
}, [ isLoading ] );
return ( return (
<> <>
{ isLoading ? <CanvasSpinner /> : null } { isLoading ? <CanvasSpinner /> : null }

View File

@ -12,7 +12,7 @@ import { store as editSiteStore } from '@wordpress/edit-site/build-module/store'
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { BlockInstance } from '@wordpress/blocks'; import { BlockInstance } from '@wordpress/blocks';
type ChangeHandler = ( export type ChangeHandler = (
blocks: BlockInstance[], blocks: BlockInstance[],
options: Record< string, unknown > options: Record< string, unknown >
) => void; ) => void;

View File

@ -6,7 +6,7 @@
*/ */
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks'; import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks';
import { useEffect } from '@wordpress/element'; import { useCallback, useEffect } from '@wordpress/element';
export const useEditorScroll = ( { export const useEditorScroll = ( {
editorSelector, editorSelector,
@ -17,21 +17,29 @@ export const useEditorScroll = ( {
} ) => { } ) => {
const isEditorLoading = useIsSiteEditorLoading(); const isEditorLoading = useIsSiteEditorLoading();
useEffect( () => { const scroll = useCallback( () => {
// Scroll to the bottom of the preview when the editor is done loading.
if ( isEditorLoading ) {
return;
}
const previewContainer = const previewContainer =
document.querySelector< HTMLIFrameElement >( editorSelector ); document.querySelector< HTMLIFrameElement >( editorSelector );
if ( previewContainer ) { if ( previewContainer ) {
previewContainer.contentWindow?.scrollTo( previewContainer.contentWindow?.scrollTo( {
0, left: 0,
top:
scrollDirection === 'bottom' scrollDirection === 'bottom'
? previewContainer.contentDocument?.body.scrollHeight || 0 ? previewContainer.contentDocument?.body.scrollHeight ||
: 0 0
); : 0,
} );
} }
}, [ isEditorLoading, editorSelector, scrollDirection ] ); }, [ scrollDirection, editorSelector ] );
useEffect( () => {
// Scroll to the bottom of the preview when the editor is done loading.
if ( ! isEditorLoading ) {
scroll();
}
}, [ isEditorLoading, scroll ] );
return {
scroll,
};
}; };

View File

@ -36,10 +36,14 @@ export const useScrollOpacity = (
? ( targetElement as Document ).documentElement ? ( targetElement as Document ).documentElement
: ( targetElement as Element ); : ( targetElement as Element );
const _sensitivity =
// Set sensitivity to a small threshold for mobile devices because they have a small viewport to ensure the effect is visible.
contentElement.clientWidth > 480 ? sensitivity : 0.05;
const maxScrollHeight = const maxScrollHeight =
contentElement.scrollHeight - contentElement.clientHeight; contentElement.scrollHeight - contentElement.clientHeight;
const currentScrollPosition = contentElement.scrollTop; const currentScrollPosition = contentElement.scrollTop;
const maxEffectScroll = maxScrollHeight * sensitivity; const maxEffectScroll = maxScrollHeight * _sensitivity;
let calculatedOpacity; let calculatedOpacity;
if ( direction === 'bottomUp' ) { if ( direction === 'bottomUp' ) {

View File

@ -53,16 +53,11 @@ const { useGlobalStyle } = unlock( blockEditorPrivateApis );
const ANIMATION_DURATION = 0.5; const ANIMATION_DURATION = 0.5;
export const Layout = () => { export const Layout = () => {
const [ logoBlock, setLogoBlock ] = useState< { const [ logoBlockIds, setLogoBlockIds ] = useState< Array< string > >( [] );
clientId: string | null;
isLoading: boolean;
} >( {
clientId: null,
isLoading: true,
} );
// This ensures the edited entity id and type are initialized properly. // This ensures the edited entity id and type are initialized properly.
useInitEditedEntityFromURL(); useInitEditedEntityFromURL();
const { shouldTourBeShown, ...onboardingTourProps } = useOnboardingTour(); const { shouldTourBeShown, isResizeHandleVisible, ...onboardingTourProps } =
useOnboardingTour();
const isMobileViewport = useViewportMatch( 'medium', '<' ); const isMobileViewport = useViewportMatch( 'medium', '<' );
const disableMotion = useReducedMotion(); const disableMotion = useReducedMotion();
@ -97,8 +92,8 @@ export const Layout = () => {
return ( return (
<LogoBlockContext.Provider <LogoBlockContext.Provider
value={ { value={ {
logoBlock, logoBlockIds,
setLogoBlock, setLogoBlockIds,
} } } }
> >
<HighlightedBlockContextProvider> <HighlightedBlockContextProvider>
@ -185,9 +180,9 @@ export const Layout = () => {
isReady={ isReady={
! isEditorLoading ! isEditorLoading
} }
duringGuideTour={ isHandleVisibleByDefault={
shouldTourBeShown && ! onboardingTourProps.showWelcomeTour &&
! onboardingTourProps.showWelcomeTour isResizeHandleVisible
} }
isFullWidth={ false } isFullWidth={ false }
defaultSize={ { defaultSize={ {
@ -217,7 +212,7 @@ export const Layout = () => {
) } ) }
</div> </div>
</div> </div>
{ shouldTourBeShown && ( { ! isEditorLoading && shouldTourBeShown && (
<OnboardingTour { ...onboardingTourProps } /> <OnboardingTour { ...onboardingTourProps } />
) } ) }
</EntityProvider> </EntityProvider>

View File

@ -4,18 +4,9 @@
import { createContext } from '@wordpress/element'; import { createContext } from '@wordpress/element';
export const LogoBlockContext = createContext< { export const LogoBlockContext = createContext< {
logoBlock: { logoBlockIds: Array< string >;
clientId: string | null; setLogoBlockIds: ( clientIds: Array< string > ) => void;
isLoading: boolean;
};
setLogoBlock: ( newBlock: {
clientId: string | null;
isLoading: boolean;
} ) => void;
} >( { } >( {
logoBlock: { logoBlockIds: [],
clientId: null, setLogoBlockIds: () => {},
isLoading: false,
},
setLogoBlock: () => {},
} ); } );

View File

@ -11,12 +11,14 @@ type OnboardingTourProps = {
onClose: () => void; onClose: () => void;
showWelcomeTour: boolean; showWelcomeTour: boolean;
setShowWelcomeTour: ( show: boolean ) => void; setShowWelcomeTour: ( show: boolean ) => void;
setIsResizeHandleVisible: ( isVisible: boolean ) => void;
}; };
export const OnboardingTour = ( { export const OnboardingTour = ( {
onClose, onClose,
setShowWelcomeTour, setShowWelcomeTour,
showWelcomeTour, showWelcomeTour,
setIsResizeHandleVisible,
}: OnboardingTourProps ) => { }: OnboardingTourProps ) => {
const [ placement, setPlacement ] = const [ placement, setPlacement ] =
useState< TourKitTypes.WooConfig[ 'placement' ] >( 'left' ); useState< TourKitTypes.WooConfig[ 'placement' ] >( 'left' );
@ -127,9 +129,11 @@ export const OnboardingTour = ( {
callbacks: { callbacks: {
onPreviousStep: () => { onPreviousStep: () => {
setPlacement( 'left' ); setPlacement( 'left' );
setIsResizeHandleVisible( true );
}, },
onNextStep: () => { onNextStep: () => {
setPlacement( 'right-start' ); setPlacement( 'right-start' );
setIsResizeHandleVisible( false );
}, },
}, },
popperModifiers: [ popperModifiers: [

View File

@ -16,6 +16,7 @@ describe( 'OnboardingTour', () => {
onClose: jest.Mock; onClose: jest.Mock;
setShowWelcomeTour: jest.Mock; setShowWelcomeTour: jest.Mock;
showWelcomeTour: boolean; showWelcomeTour: boolean;
setIsResizeHandleVisible: ( isVisible: boolean ) => void;
}; };
beforeEach( () => { beforeEach( () => {
@ -23,6 +24,7 @@ describe( 'OnboardingTour', () => {
onClose: jest.fn(), onClose: jest.fn(),
setShowWelcomeTour: jest.fn(), setShowWelcomeTour: jest.fn(),
showWelcomeTour: true, showWelcomeTour: true,
setIsResizeHandleVisible: jest.fn(),
}; };
} ); } );

View File

@ -10,6 +10,8 @@ export const CUSTOMIZE_STORE_ONBOARDING_TOUR_HIDDEN =
export const useOnboardingTour = () => { export const useOnboardingTour = () => {
const [ showWelcomeTour, setShowWelcomeTour ] = useState( true ); const [ showWelcomeTour, setShowWelcomeTour ] = useState( true );
const [ isResizeHandleVisible, setIsResizeHandleVisible ] =
useState( true );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { shouldTourBeShown } = useSelect( ( select ) => { const { shouldTourBeShown } = useSelect( ( select ) => {
@ -38,5 +40,7 @@ export const useOnboardingTour = () => {
shouldTourBeShown, shouldTourBeShown,
showWelcomeTour, showWelcomeTour,
setShowWelcomeTour, setShowWelcomeTour,
setIsResizeHandleVisible,
isResizeHandleVisible,
}; };
}; };

View File

@ -75,7 +75,7 @@ function ResizableFrame( {
/** The default (unresized) width/height of the frame, based on the space availalbe in the viewport. */ /** The default (unresized) width/height of the frame, based on the space availalbe in the viewport. */
defaultSize, defaultSize,
innerContentStyle, innerContentStyle,
duringGuideTour = false, isHandleVisibleByDefault = false,
} ) { } ) {
const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE ); const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE );
// The width of the resizable frame when a new resize gesture starts. // The width of the resizable frame when a new resize gesture starts.
@ -192,7 +192,9 @@ function ResizableFrame( {
if ( isResizing ) { if ( isResizing ) {
return 'active'; return 'active';
} }
return shouldShowHandle || duringGuideTour ? 'visible' : 'hidden'; return shouldShowHandle || isHandleVisibleByDefault
? 'visible'
: 'hidden';
} )(); } )();
const resizeHandler = ( const resizeHandler = (
@ -220,10 +222,10 @@ function ResizableFrame( {
whileFocus="active" whileFocus="active"
whileHover="active" whileHover="active"
children={ children={
duringGuideTour && isHandleVisibleByDefault &&
! hasHandlerDragged && ( ! hasHandlerDragged && (
<Popover <Popover
className="components-tooltip" className="woocommerce-assembler-hub__resizable-frame__drag-handler"
position="middle right" position="middle right"
> >
{ __( 'Drag to resize', 'woocommerce' ) } { __( 'Drag to resize', 'woocommerce' ) }
@ -273,7 +275,7 @@ function ResizableFrame( {
handleComponent={ { handleComponent={ {
left: ( left: (
<> <>
{ duringGuideTour ? ( { isHandleVisibleByDefault ? (
<div>{ resizeHandler }</div> <div>{ resizeHandler }</div>
) : ( ) : (
<Tooltip <Tooltip

View File

@ -69,6 +69,45 @@ export const FONT_PAIRINGS = [
}, },
}, },
}, },
{
title: 'Albert Sans + Lora',
version: 2,
lookAndFeel: [ 'Contemporary', 'Bold' ] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Albert Sans',
slug: 'albert-sans',
},
{
fontFamily: 'Lora',
slug: 'lora',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--albert-sans)',
fontStyle: 'normal',
fontWeight: '700',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--lora)',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '1.67',
},
},
},
{ {
title: 'Bodoni Moda + Overpass', title: 'Bodoni Moda + Overpass',
version: 2, version: 2,
@ -193,10 +232,120 @@ export const FONT_PAIRINGS = [
}, },
}, },
}, },
{
title: 'Cormorant + Work Sans',
version: 2,
lookAndFeel: [] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Cormorant',
slug: 'cormorant',
},
{
fontFamily: 'Work Sans',
slug: 'work-sans',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily: 'var(--wp--preset--font-family--cormorant)',
fontStyle: 'normal',
fontWeight: '500',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--work-sans)',
},
},
},
{
title: 'DM Sans + IBM Plex Mono',
version: 2,
lookAndFeel: [] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'DM Sans',
slug: 'dm-sans',
},
{
fontFamily: 'IBM Plex Mono',
slug: 'ibm-plex-mono',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily: 'var(--wp--preset--font-family--dm-sans)',
fontStyle: 'normal',
fontWeight: '700',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--ibm-plex-mono)',
fontSize: 'var(--wp--preset--font-size--small)',
fontStyle: 'normal',
fontWeight: '300',
lineHeight: '1.67',
},
},
},
{
title: 'Fraunces + Libre Franklin',
version: 2,
lookAndFeel: [ 'Classic' ] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Fraunces',
slug: 'fraunces',
},
{
fontFamily: 'Libre Franklin',
slug: 'libre-franklin',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily: 'var(--wp--preset--font-family--fraunces)',
fontStyle: 'normal',
fontWeight: '500',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--libre-franklin)',
lineHeight: '1.67',
},
},
},
{ {
title: 'Libre Baskerville + DM Sans', title: 'Libre Baskerville + DM Sans',
version: 2, version: 2,
lookAndFeel: [ 'Classic', 'Bold' ] as Look[], lookAndFeel: [] as Look[],
settings: { settings: {
typography: { typography: {
fontFamilies: { fontFamilies: {
@ -257,7 +406,7 @@ export const FONT_PAIRINGS = [
{ {
title: 'Libre Franklin + EB Garamond', title: 'Libre Franklin + EB Garamond',
version: 2, version: 2,
lookAndFeel: [ 'Contemporary', 'Classic', 'Bold' ] as Look[], lookAndFeel: [ 'Classic' ] as Look[],
settings: { settings: {
typography: { typography: {
fontFamilies: { fontFamilies: {
@ -382,6 +531,40 @@ export const FONT_PAIRINGS = [
}, },
}, },
}, },
{
title: 'Newsreader + Newsreader',
version: 2,
lookAndFeel: [ 'Classic' ] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Newsreader',
slug: 'newsreader',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--newsreader)',
fontStyle: 'normal',
fontWeight: '400',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--newsreader)',
fontSize: 'var(--wp--preset--font-size--medium)',
lineHeight: '1.67',
},
},
},
{ {
title: 'Playfair Display + Fira Sans', title: 'Playfair Display + Fira Sans',
version: 2, version: 2,
@ -448,9 +631,79 @@ export const FONT_PAIRINGS = [
}, },
}, },
{ {
title: 'Rubik + Inter', title: 'Plus Jakarta Sans + Plus Jakarta Sans',
version: 2, version: 2,
lookAndFeel: [ 'Contemporary', 'Bold' ] as Look[], lookAndFeel: [ 'Contemporary', 'Bold' ] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Plus Jakarta Sans',
slug: 'plus-jakarta-sans',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily:
'var(--wp--preset--font-family--plus-jakarta-sans)',
fontStyle: 'normal',
fontWeight: '700',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--plus-jakarta-sans)',
lineHeight: '1.67',
},
},
},
{
title: 'Raleway + Cormorant',
version: 2,
lookAndFeel: [ 'Classic', 'Bold' ] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Raleway',
slug: 'raleway',
},
{
fontFamily: 'Cormorant',
slug: 'cormorant',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily: 'var(--wp--preset--font-family--raleway)',
fontStyle: 'normal',
fontWeight: '700',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--cormorant)',
fontSize: 'var(--wp--preset--font-size--medium)',
lineHeight: '1.67',
},
},
},
{
title: 'Rubik + Inter',
version: 2,
lookAndFeel: [ 'Bold' ] as Look[],
settings: { settings: {
typography: { typography: {
fontFamilies: { fontFamilies: {
@ -506,10 +759,41 @@ export const FONT_PAIRINGS = [
}, },
}, },
}, },
{
title: 'Rubik + Rubik',
version: 2,
lookAndFeel: [ 'Contemporary' ] as Look[],
settings: {
typography: {
fontFamilies: {
theme: [
{
fontFamily: 'Rubik',
slug: 'rubik',
},
],
},
},
},
styles: {
elements: {
heading: {
typography: {
fontFamily: 'var(--wp--preset--font-family--rubik)',
},
},
},
typography: {
fontFamily: 'var(--wp--preset--font-family--rubik)',
fontWeight: '400',
lineHeight: '1.67',
},
},
},
{ {
title: 'Space Mono + Roboto', title: 'Space Mono + Roboto',
version: 2, version: 2,
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [] as Look[],
settings: { settings: {
typography: { typography: {
fontFamilies: { fontFamilies: {

View File

@ -37,7 +37,7 @@ const SUPPORTED_FOOTER_PATTERNS = [
]; ];
export const SidebarNavigationScreenFooter = () => { export const SidebarNavigationScreenFooter = () => {
useEditorScroll( { const { scroll } = useEditorScroll( {
editorSelector: '.woocommerce-customize-store__block-editor iframe', editorSelector: '.woocommerce-customize-store__block-editor iframe',
scrollDirection: 'bottom', scrollDirection: 'bottom',
} ); } );
@ -81,7 +81,6 @@ export const SidebarNavigationScreenFooter = () => {
blocks[ blocks.length - 1 ] blocks[ blocks.length - 1 ]
); );
setSelectedPattern( currentSelectedPattern ); setSelectedPattern( currentSelectedPattern );
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-run this effect when currentSelectedPattern changes // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-run this effect when currentSelectedPattern changes
}, [ blocks, footerPatterns ] ); }, [ blocks, footerPatterns ] );
@ -91,8 +90,9 @@ export const SidebarNavigationScreenFooter = () => {
onChange( [ ...blocks.slice( 0, -1 ), selectedBlocks[ 0 ] ], { onChange( [ ...blocks.slice( 0, -1 ), selectedBlocks[ 0 ] ], {
selection: {}, selection: {},
} ); } );
scroll();
}, },
[ blocks, onChange, setSelectedPattern ] [ blocks, onChange, setSelectedPattern, scroll ]
); );
return ( return (

View File

@ -37,7 +37,7 @@ const SUPPORTED_HEADER_PATTERNS = [
]; ];
export const SidebarNavigationScreenHeader = () => { export const SidebarNavigationScreenHeader = () => {
useEditorScroll( { const { scroll } = useEditorScroll( {
editorSelector: '.woocommerce-customize-store__block-editor iframe', editorSelector: '.woocommerce-customize-store__block-editor iframe',
scrollDirection: 'top', scrollDirection: 'top',
} ); } );
@ -78,6 +78,7 @@ export const SidebarNavigationScreenHeader = () => {
blocks[ 0 ] blocks[ 0 ]
); );
setSelectedPattern( currentSelectedPattern ); setSelectedPattern( currentSelectedPattern );
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-run this effect when currentSelectedPattern changes // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-run this effect when currentSelectedPattern changes
}, [ blocks, headerPatterns ] ); }, [ blocks, headerPatterns ] );
@ -87,8 +88,9 @@ export const SidebarNavigationScreenHeader = () => {
onChange( [ selectedBlocks[ 0 ], ...blocks.slice( 1 ) ], { onChange( [ selectedBlocks[ 0 ], ...blocks.slice( 1 ) ], {
selection: {}, selection: {},
} ); } );
scroll();
}, },
[ blocks, onChange, setSelectedPattern ] [ blocks, onChange, setSelectedPattern, scroll ]
); );
return ( return (

View File

@ -26,8 +26,13 @@ import { useEditorBlocks } from '../hooks/use-editor-blocks';
import { useHomeTemplates } from '../hooks/use-home-templates'; import { useHomeTemplates } from '../hooks/use-home-templates';
import { BlockInstance } from '@wordpress/blocks'; import { BlockInstance } from '@wordpress/blocks';
import { useSelectedPattern } from '../hooks/use-selected-pattern'; import { useSelectedPattern } from '../hooks/use-selected-pattern';
import { useEditorScroll } from '../hooks/use-editor-scroll';
export const SidebarNavigationScreenHomepage = () => { export const SidebarNavigationScreenHomepage = () => {
const { scroll } = useEditorScroll( {
editorSelector: '.woocommerce-customize-store__block-editor iframe',
scrollDirection: 'top',
} );
const { isLoading, homeTemplates } = useHomeTemplates(); const { isLoading, homeTemplates } = useHomeTemplates();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const { selectedPattern, setSelectedPattern } = useSelectedPattern(); const { selectedPattern, setSelectedPattern } = useSelectedPattern();
@ -40,8 +45,9 @@ export const SidebarNavigationScreenHomepage = () => {
[ blocks[ 0 ], ...selectedBlocks, blocks[ blocks.length - 1 ] ], [ blocks[ 0 ], ...selectedBlocks, blocks[ blocks.length - 1 ] ],
{ selection: {} } { selection: {} }
); );
scroll();
}, },
[ blocks, onChange, setSelectedPattern ] [ blocks, onChange, setSelectedPattern, scroll ]
); );
const homePatterns = useMemo( () => { const homePatterns = useMemo( () => {
@ -82,6 +88,7 @@ export const SidebarNavigationScreenHomepage = () => {
} ); } );
setSelectedPattern( _currentSelectedPattern ); setSelectedPattern( _currentSelectedPattern );
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-run this effect when currentSelectedPattern changes // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-run this effect when currentSelectedPattern changes
}, [ blocks, homePatterns ] ); }, [ blocks, homePatterns ] );
@ -130,7 +137,7 @@ export const SidebarNavigationScreenHomepage = () => {
orientation="vertical" orientation="vertical"
category={ 'homepage' } category={ 'homepage' }
isDraggable={ false } isDraggable={ false }
showTitlesAsTooltip={ true } showTitlesAsTooltip={ false }
/> />
) } ) }
</div> </div>

View File

@ -176,17 +176,11 @@ const LogoSettings = ( {
const isWideAligned = [ 'wide', 'full' ].includes( align ); const isWideAligned = [ 'wide', 'full' ].includes( align );
const isResizable = ! isWideAligned && isLargeViewport; const isResizable = ! isWideAligned && isLargeViewport;
const { maxWidth } = useSelect( ( select ) => { const maxWidth = 200;
// @ts-ignore No types for this exist yet.
const settings = select( blockEditorStore ).getSettings();
return {
maxWidth: settings.maxWidth,
};
}, [] );
// Set the default width to a responsible size. // Set the default width to a responsible size.
// Note that this width is also set in the attached frontend CSS file. // Note that this width is also set in the attached frontend CSS file.
const defaultWidth = 120; const defaultWidth = 60;
const currentWidth = width || defaultWidth; const currentWidth = width || defaultWidth;
const ratio = naturalWidth / naturalHeight; const ratio = naturalWidth / naturalHeight;
@ -218,7 +212,7 @@ const LogoSettings = ( {
setAttributes( { width: newWidth } ) setAttributes( { width: newWidth } )
} }
min={ minWidth } min={ minWidth }
max={ maxWidthBuffer } max={ maxWidth }
initialPosition={ Math.min( defaultWidth, maxWidthBuffer ) } initialPosition={ Math.min( defaultWidth, maxWidthBuffer ) }
value={ currentWidth } value={ currentWidth }
disabled={ ! isResizable } disabled={ ! isResizable }
@ -367,9 +361,7 @@ const LogoEdit = ( {
export const SidebarNavigationScreenLogo = () => { export const SidebarNavigationScreenLogo = () => {
// Get the current logo block client ID and attributes. These are used for the logo settings. // Get the current logo block client ID and attributes. These are used for the logo settings.
const { const { logoBlockIds } = useContext( LogoBlockContext );
logoBlock: { clientId, isLoading: isLogoBlockLoading },
} = useContext( LogoBlockContext );
const { const {
attributes, attributes,
@ -381,7 +373,7 @@ export const SidebarNavigationScreenLogo = () => {
( select ) => { ( select ) => {
const logoBlocks = const logoBlocks =
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
select( blockEditorStore ).getBlocksByClientId( clientId ); select( blockEditorStore ).getBlocksByClientId( logoBlockIds );
const _isAttributesLoading = const _isAttributesLoading =
! logoBlocks.length || logoBlocks[ 0 ] === null; ! logoBlocks.length || logoBlocks[ 0 ] === null;
@ -398,7 +390,7 @@ export const SidebarNavigationScreenLogo = () => {
isAttributesLoading: _isAttributesLoading, isAttributesLoading: _isAttributesLoading,
}; };
}, },
[ clientId ] [ logoBlockIds ]
); );
const { siteLogoId, canUserEdit, mediaItemData, isRequestingMediaItem } = const { siteLogoId, canUserEdit, mediaItemData, isRequestingMediaItem } =
@ -441,16 +433,16 @@ export const SidebarNavigationScreenLogo = () => {
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
const { updateBlockAttributes } = useDispatch( blockEditorStore ); const { updateBlockAttributes } = useDispatch( blockEditorStore );
const setAttributes = ( newAttributes: LogoAttributes ) => { const setAttributes = ( newAttributes: LogoAttributes ) => {
if ( ! clientId ) { if ( ! logoBlockIds.length ) {
return; return;
} }
updateBlockAttributes( clientId, newAttributes ); logoBlockIds.forEach( ( clientId ) =>
updateBlockAttributes( clientId, newAttributes )
);
}; };
const isLoading = const isLoading =
siteLogoId === undefined || siteLogoId === undefined ||
isRequestingMediaItem || isRequestingMediaItem ||
isLogoBlockLoading ||
isAttributesLoading; isAttributesLoading;
return ( return (

View File

@ -21,7 +21,8 @@ import { decodeEntities } from '@wordpress/html-entities';
import { forwardRef } from '@wordpress/element'; import { forwardRef } from '@wordpress/element';
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
import SiteIcon from '@wordpress/edit-site/build-module/components/site-icon'; import SiteIcon from '@wordpress/edit-site/build-module/components/site-icon';
import { getNewPath } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
/** /**
* Internal dependencies * Internal dependencies
*/ */
@ -92,8 +93,13 @@ export const SiteHub = forwardRef(
: HUB_ANIMATION_DURATION, : HUB_ANIMATION_DURATION,
ease: 'easeOut', ease: 'easeOut',
} } } }
>
<Link
href={ getNewPath( {}, '/', {} ) }
type="wp-admin"
> >
<SiteIcon className="edit-site-layout__view-mode-toggle-icon" /> <SiteIcon className="edit-site-layout__view-mode-toggle-icon" />
</Link>
</motion.div> </motion.div>
<AnimatePresence> <AnimatePresence>

View File

@ -53,6 +53,12 @@
} }
} }
.woocommerce-customize-store {
.edit-site-site-hub__view-mode-toggle-container a {
color: unset;
}
}
.woocommerce-customize-store__step-assemblerHub { .woocommerce-customize-store__step-assemblerHub {
a { a {
text-decoration: none; text-decoration: none;
@ -84,6 +90,7 @@
padding-bottom: 0; padding-bottom: 0;
gap: 0; gap: 0;
width: 348px; width: 348px;
z-index: 2;
} }
.edit-site-sidebar-navigation-screen-patterns__group-header { .edit-site-sidebar-navigation-screen-patterns__group-header {
@ -141,6 +148,20 @@
padding: 0 16px; padding: 0 16px;
overflow-x: hidden; overflow-x: hidden;
flex: 1; flex: 1;
&::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
}
&::-webkit-scrollbar-track-piece:start {
background: transparent;
margin-top: 250px;
}
&::-webkit-scrollbar-track-piece:end {
background: transparent;
margin-bottom: 400px;
}
} }
} }
@ -354,6 +375,10 @@
} }
} }
.block-editor-block-patterns-list__item-title {
display: none;
}
/* Color sidebar */ /* Color sidebar */
.woocommerce-customize-store__color-panel-container { .woocommerce-customize-store__color-panel-container {
@ -445,7 +470,7 @@
/* Layout sidebar */ /* Layout sidebar */
.block-editor-block-patterns-list__item { .block-editor-block-patterns-list__item {
.block-editor-block-preview__container { .block-editor-block-preview__container {
border-radius: 2px; border-radius: 4px;
border: 1.5px solid transparent; border: 1.5px solid transparent;
} }
@ -480,27 +505,7 @@
.edit-site-resizable-frame__handle { .edit-site-resizable-frame__handle {
background: var(--wp-admin-theme-color); background: var(--wp-admin-theme-color);
cursor: ew-resize;
.components-popover {
display: inline-flex;
padding: 0 10px;
align-items: flex-start;
gap: 10px;
background: var(--wp-admin-theme-color-background-25);
left: 5px !important;
border-radius: 4px;
height: 20px;
.components-popover__content {
color: var(--wp-admin-theme-color-darker-20);
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
background: inherit;
padding: 0;
}
}
} }
.edit-site-layout__canvas .components-resizable-box__container { .edit-site-layout__canvas .components-resizable-box__container {
@ -660,3 +665,28 @@
stroke: rgba($black, 0.3); stroke: rgba($black, 0.3);
} }
} }
.woocommerce-assembler-hub__resizable-frame__drag-handler {
display: inline-flex;
padding: 0 10px;
align-items: flex-start;
gap: 10px;
background: var(--wp-admin-theme-color-background-25);
left: 5px !important;
border-radius: 4px;
height: 20px;
.components-popover__content {
color: var(--wp-admin-theme-color-darker-20);
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
background: inherit;
padding: 0;
margin-left: 10px;
width: max-content;
box-shadow: none;
}
}

View File

@ -29,6 +29,7 @@ import {
lookAndFeelCompleteEvent, lookAndFeelCompleteEvent,
toneOfVoiceCompleteEvent, toneOfVoiceCompleteEvent,
} from './pages'; } from './pages';
import { attachIframeListeners, onIframeLoad } from '../utils';
const assignBusinessInfoDescription = assign< const assignBusinessInfoDescription = assign<
designWithAiStateMachineContext, designWithAiStateMachineContext,
@ -116,7 +117,7 @@ const assignFontPairing = assign<
fontPairing = 'Bodoni Moda + Overpass'; fontPairing = 'Bodoni Moda + Overpass';
break; break;
case choice === 'Bold': case choice === 'Bold':
fontPairing = 'Rubik + Inter'; fontPairing = 'Plus Jakarta Sans + Plus Jakarta Sans';
break; break;
} }
@ -276,12 +277,33 @@ const recordTracksStepCompleted = (
} ); } );
}; };
const redirectToAssemblerHub = () => { const redirectToAssemblerHub = async () => {
window.location.href = getNewPath( const assemblerUrl = getNewPath( {}, '/customize-store/assembler-hub', {} );
{}, const iframe = document.createElement( 'iframe' );
'/customize-store/assembler-hub', iframe.classList.add( 'cys-fullscreen-iframe' );
{} iframe.src = assemblerUrl;
const showIframe = () => {
const loader = document.getElementsByClassName(
'woocommerce-onboarding-loader'
); );
if ( loader[ 0 ] ) {
( loader[ 0 ] as HTMLElement ).style.display = 'none';
}
iframe.style.opacity = '1';
};
iframe.onload = () => {
// Hide loading UI
attachIframeListeners( iframe );
onIframeLoad( showIframe );
// Ceiling wait time set to 60 seconds
setTimeout( showIframe, 60 * 1000 );
window.history?.pushState( {}, '', assemblerUrl );
};
document.body.appendChild( iframe );
}; };
export const actions = { export const actions = {

View File

@ -91,7 +91,7 @@ export const ApiCallLoader = () => {
return ( return (
<Loader> <Loader>
<Loader.Sequence interval={ 3500 } shouldLoop={ false }> <Loader.Sequence interval={ 4500 } shouldLoop={ false }>
{ loaderSteps.map( ( step, index ) => ( { loaderSteps.map( ( step, index ) => (
<Loader.Layout key={ index }> <Loader.Layout key={ index }>
<Loader.Illustration> <Loader.Illustration>

View File

@ -17,12 +17,20 @@ const colorChoices: ColorPalette[] = [
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{ {
name: 'Crimson Tide', name: 'Arctic Dawn',
primary: '#A02040', primary: '#243156',
secondary: '#234B57', secondary: '#DE5853',
foreground: '#871C37', foreground: '#243156',
background: '#ffffff', background: '#ffffff',
lookAndFeel: [ 'Bold' ] as Look[], lookAndFeel: [ 'Contemporary' ] as Look[],
},
{
name: 'Bronze Serenity',
primary: '#1e4b4b',
secondary: '#9e7047',
foreground: '#1e4b4b',
background: '#ffffff',
lookAndFeel: [ 'Classic' ],
}, },
{ {
name: 'Purple Twilight', name: 'Purple Twilight',
@ -32,76 +40,28 @@ const colorChoices: ColorPalette[] = [
background: '#fefbff', background: '#fefbff',
lookAndFeel: [ 'Bold' ] as Look[], lookAndFeel: [ 'Bold' ] as Look[],
}, },
{
name: 'Candy Store',
primary: '#293852',
secondary: '#f1bea7',
foreground: '#293852',
background: '#ffffff',
lookAndFeel: [ 'Classic' ],
},
{ {
name: 'Midnight Citrus', name: 'Midnight Citrus',
primary: '#1B1736', primary: '#1B1736',
secondary: '#7E76A3', secondary: '#7E76A3',
foreground: '#1B1736', foreground: '#1B1736',
background: '#ffffff', background: '#ffffff',
lookAndFeel: [ 'Bold' ] as Look[], lookAndFeel: [ 'Bold', 'Contemporary' ] as Look[],
}, },
{ {
name: 'Lemon Myrtle', name: 'Crimson Tide',
primary: '#3E7172', primary: '#A02040',
secondary: '#FC9B00', secondary: '#234B57',
foreground: '#325C5D', foreground: '#871C37',
background: '#ffffff', background: '#ffffff',
lookAndFeel: [ 'Contemporary' ] as Look[],
},
{
name: 'Green Thumb',
primary: '#164A41',
secondary: '#4B7B4D',
foreground: '#164A41',
background: '#ffffff',
lookAndFeel: [ 'Contemporary' ] as Look[],
},
{
name: 'Golden Haze',
primary: '#232224',
secondary: '#EBB54F',
foreground: '#515151',
background: '#ffffff',
lookAndFeel: [ 'Contemporary', 'Bold' ] as Look[],
},
{
name: 'Golden Indigo',
primary: '#4866C0',
secondary: '#C09F50',
foreground: '#405AA7',
background: '#ffffff',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
},
{
name: 'Arctic Dawn',
primary: '#243156',
secondary: '#DE5853',
foreground: '#243156',
background: '#ffffff',
lookAndFeel: [ 'Contemporary' ] as Look[],
},
{
name: 'Jungle Sunrise',
primary: '#1a4435',
secondary: '#ed774e',
foreground: '#0a271d',
background: '#fefbec',
lookAndFeel: [ 'Classic' ] as Look[],
},
{
name: 'Berry Grove',
primary: '#1F351A',
secondary: '#DE76DE',
foreground: '#1f351a',
background: '#fdfaf1',
lookAndFeel: [ 'Classic' ] as Look[],
},
{
name: 'Fuchsia',
primary: '#b7127f',
secondary: '#18020C',
foreground: '#b7127f',
background: '#f7edf6',
lookAndFeel: [ 'Bold' ] as Look[], lookAndFeel: [ 'Bold' ] as Look[],
}, },
{ {
@ -112,29 +72,21 @@ const colorChoices: ColorPalette[] = [
background: '#eeeae6', background: '#eeeae6',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{
name: 'Canary',
primary: '#0F0F05',
secondary: '#353535',
foreground: '#0F0F05',
background: '#FCFF9B',
lookAndFeel: [ 'Bold' ] as Look[],
},
{ {
name: 'Gumtree Sunset', name: 'Gumtree Sunset',
primary: '#476C77', primary: '#476C77',
secondary: '#EFB071', secondary: '#EFB071',
foreground: '#476C77', foreground: '#476C77',
background: '#edf4f4', background: '#edf4f4',
lookAndFeel: [ 'Contemporary' ] as Look[], lookAndFeel: [ 'Classic' ] as Look[],
}, },
{ {
name: 'Ice', name: 'Fuchsia',
primary: '#12123F', primary: '#b7127f',
secondary: '#3473FE', secondary: '#18020C',
foreground: '#12123F', foreground: '#b7127f',
background: '#F1F4FA', background: '#f7edf6',
lookAndFeel: [ 'Contemporary' ] as Look[], lookAndFeel: [ 'Bold' ] as Look[],
}, },
{ {
name: 'Cinder', name: 'Cinder',
@ -144,6 +96,14 @@ const colorChoices: ColorPalette[] = [
background: '#f1f2f2', background: '#f1f2f2',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{
name: 'Canary',
primary: '#0F0F05',
secondary: '#353535',
foreground: '#0F0F05',
background: '#FCFF9B',
lookAndFeel: [ 'Bold' ] as Look[],
},
{ {
name: 'Blue Lagoon', name: 'Blue Lagoon',
primary: '#004DE5', primary: '#004DE5',
@ -153,19 +113,27 @@ const colorChoices: ColorPalette[] = [
lookAndFeel: [ 'Bold', 'Contemporary' ] as Look[], lookAndFeel: [ 'Bold', 'Contemporary' ] as Look[],
}, },
{ {
name: 'Sandalwood Oasis', name: 'Vibrant Berry',
primary: '#F0EBE3', primary: '#7C1D6F',
secondary: '#DF9785', secondary: '#C62FB2',
foreground: '#ffffff', foreground: '#7C1D6F',
background: '#2a2a16', background: '#FFEED6',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Classic', 'Bold' ],
}, },
{ {
name: 'Rustic Rosewood', name: 'Aquamarine Night',
primary: '#F4F4F2', primary: '#deffef',
secondary: '#EE797C', secondary: '#56fbb9',
foreground: '#ffffff', foreground: '#ffffff',
background: '#1A1A1A', background: '#091C48',
lookAndFeel: [ 'Bold' ] as Look[],
},
{
name: 'Evergreen Twilight',
primary: '#ffffff',
secondary: '#8EE978',
foreground: '#ffffff',
background: '#181818',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{ {
@ -176,14 +144,6 @@ const colorChoices: ColorPalette[] = [
background: '#3C3F4D', background: '#3C3F4D',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{
name: 'Lilac Nightshade',
primary: '#f5d6ff',
secondary: '#C48DDA',
foreground: '#ffffff',
background: '#000000',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
},
{ {
name: 'Lightning', name: 'Lightning',
primary: '#ebffd2', primary: '#ebffd2',
@ -193,12 +153,12 @@ const colorChoices: ColorPalette[] = [
lookAndFeel: [ 'Bold' ] as Look[], lookAndFeel: [ 'Bold' ] as Look[],
}, },
{ {
name: 'Aquamarine Night', name: 'Lilac Nightshade',
primary: '#deffef', primary: '#f5d6ff',
secondary: '#56fbb9', secondary: '#C48DDA',
foreground: '#ffffff', foreground: '#ffffff',
background: '#091C48', background: '#000000',
lookAndFeel: [ 'Bold' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{ {
name: 'Charcoal', name: 'Charcoal',
@ -209,11 +169,19 @@ const colorChoices: ColorPalette[] = [
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{ {
name: 'Evergreen Twilight', name: 'Rustic Rosewood',
primary: '#ffffff', primary: '#F4F4F2',
secondary: '#8EE978', secondary: '#EE797C',
foreground: '#ffffff', foreground: '#ffffff',
background: '#181818', background: '#1A1A1A',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
},
{
name: 'Sandalwood Oasis',
primary: '#F0EBE3',
secondary: '#DF9785',
foreground: '#ffffff',
background: '#2a2a16',
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
{ {
@ -225,6 +193,7 @@ const colorChoices: ColorPalette[] = [
lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[], lookAndFeel: [ 'Contemporary', 'Classic' ] as Look[],
}, },
]; ];
const allowedNames: string[] = colorChoices.map( ( palette ) => palette.name ); const allowedNames: string[] = colorChoices.map( ( palette ) => palette.name );
const hexColorRegex = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/; const hexColorRegex = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/;
const colorPaletteNameValidator = z const colorPaletteNameValidator = z

View File

@ -186,8 +186,8 @@ describe( 'colorPaletteResponseValidator', () => {
'Lightning', 'Lightning',
'Midnight Citrus', 'Midnight Citrus',
'Purple Twilight', 'Purple Twilight',
'Crimson Tide', 'Fuchsia',
'Ice', 'Charcoal',
], ],
}; };
it( 'should validate a correct color palette response', () => { it( 'should validate a correct color palette response', () => {

View File

@ -38,6 +38,7 @@ import {
} from './types'; } from './types';
import { ThemeCard } from './intro/types'; import { ThemeCard } from './intro/types';
import './style.scss'; import './style.scss';
import { navigateOrParent, attachParentListeners } from './utils';
export type customizeStoreStateMachineEvents = export type customizeStoreStateMachineEvents =
| introEvents | introEvents
@ -65,7 +66,8 @@ const updateQueryStep = (
}; };
const redirectToWooHome = () => { const redirectToWooHome = () => {
window.location.href = getNewPath( {}, '/', {} ); const url = getNewPath( {}, '/', {} );
navigateOrParent( window, url );
}; };
const redirectToThemes = ( _context: customizeStoreStateMachineContext ) => { const redirectToThemes = ( _context: customizeStoreStateMachineContext ) => {
@ -360,6 +362,12 @@ export const CustomizeStoreController = ( {
} }
}, [ CurrentComponent, currentNodeMeta?.component ] ); }, [ CurrentComponent, currentNodeMeta?.component ] );
// Run listeners for parent window.
useEffect( () => {
const removeListener = attachParentListeners();
return removeListener;
}, [] );
const currentNodeCssLabel = const currentNodeCssLabel =
state.value instanceof Object state.value instanceof Object
? Object.keys( state.value )[ 0 ] ? Object.keys( state.value )[ 0 ]

View File

@ -10,6 +10,7 @@ import { getNewPath } from '@woocommerce/navigation';
* Internal dependencies * Internal dependencies
*/ */
import { Intro } from '.'; import { Intro } from '.';
import { navigateOrParent } from '../utils';
export const BaseIntroBanner = ( { export const BaseIntroBanner = ( {
bannerTitle, bannerTitle,
@ -190,10 +191,9 @@ export const ExistingAiThemeBanner = ( {
bannerClass="existing-ai-theme-banner" bannerClass="existing-ai-theme-banner"
buttonIsLink={ false } buttonIsLink={ false }
bannerButtonOnClick={ () => { bannerButtonOnClick={ () => {
window.location.href = getNewPath( navigateOrParent(
{}, window,
'/customize-store/assembler-hub', getNewPath( {}, '/customize-store/assembler-hub', {} )
{}
); );
} } } }
bannerButtonText={ __( 'Customize', 'woocommerce' ) } bannerButtonText={ __( 'Customize', 'woocommerce' ) }

View File

@ -21,6 +21,15 @@
.woocommerce-layout__main { .woocommerce-layout__main {
padding-right: 0; padding-right: 0;
} }
.edit-site-site-icon__image {
background: transparent;
border-radius: 4px;
height: auto;
-o-object-fit: cover;
object-fit: cover;
width: 100%;
}
} }
body.woocommerce-customize-store.js.is-fullscreen-mode { body.woocommerce-customize-store.js.is-fullscreen-mode {
@ -158,3 +167,16 @@ body.woocommerce-customize-store.js.is-fullscreen-mode {
height: initial; height: initial;
} }
} }
.cys-fullscreen-iframe {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
z-index: 9999;
transition: opacity 1.2s linear;
opacity: 0;
}

View File

@ -23,6 +23,7 @@ import { SiteHub } from '../assembler-hub/site-hub';
import { ADMIN_URL } from '~/utils/admin-settings'; import { ADMIN_URL } from '~/utils/admin-settings';
import './style.scss'; import './style.scss';
import { navigateOrParent } from '../utils';
export type events = { type: 'GO_BACK_TO_HOME' }; export type events = { type: 'GO_BACK_TO_HOME' };
@ -97,7 +98,10 @@ export const Transitional = ( {
recordEvent( recordEvent(
'customize_your_store_transitional_editor_click' 'customize_your_store_transitional_editor_click'
); );
window.location.href = `${ ADMIN_URL }site-editor.php`; navigateOrParent(
window,
`${ ADMIN_URL }site-editor.php`
);
} } } }
> >
{ __( 'Go to the Editor', 'woocommerce' ) } { __( 'Go to the Editor', 'woocommerce' ) }

View File

@ -0,0 +1,101 @@
export function sendMessageToParent( message ) {
window.parent.postMessage( message, '*' );
}
export function isIframe( windowObject ) {
return windowObject.document !== windowObject.parent.document;
}
export function editorIsLoaded() {
window.parent.postMessage( { type: 'iframe-loaded' }, '*' );
}
export function onIframeLoad( callback ) {
window.addEventListener( 'message', ( event ) => {
if ( event.data.type === 'iframe-loaded' ) {
callback();
}
} );
}
/**
* Attach a listener to the window object to listen for messages from the parent window.
*
* @return {() => void} Remove listener function
*/
export function attachParentListeners() {
const listener = ( event ) => {
if ( event.data.type === 'navigate' ) {
window.location.href = event.data.url;
}
};
window.addEventListener( 'message', listener, false );
return () => {
window.removeEventListener( 'message', listener, false );
};
}
/**
* If iframe, post message. Otherwise, navigate to a URL.
*
* @param {*} windowObject
* @param {*} url
*/
export function navigateOrParent( windowObject, url ) {
if ( isIframe( windowObject ) ) {
windowObject.parent.postMessage( { type: 'navigate', url }, '*' );
} else {
windowObject.location.href = url;
}
}
/**
* Attach listeners to an iframe to intercept and redirect navigation events.
*
* @param {HTMLIFrameElement} iframe
*/
export function attachIframeListeners( iframe ) {
const iframeWindow = iframe.contentWindow;
const iframeDocument =
iframe.contentDocument || iframe.contentWindow?.document;
// Listen for pushstate event
if ( iframeWindow?.history ) {
iframeWindow.history.pushState = function ( state, title, url ) {
const urlString = url?.toString();
if ( urlString ) {
// If the URL is not the Assembler Hub, navigate the main window to the new URL.
if ( urlString?.indexOf( 'customize-store' ) === -1 ) {
window.location.href = urlString;
} else {
window.history.pushState( state, title, url ); // Update the main window's history
}
}
};
}
// Listen for popstate event
iframeWindow?.addEventListener( 'popstate', function ( event ) {
window.history.replaceState(
event.state,
'',
iframeWindow.location.href
);
} );
// Intercept external link clicks
iframeDocument?.addEventListener( 'click', function ( event ) {
if ( event.target ) {
const anchor = event.target?.closest( 'a' );
if ( anchor && anchor.target === '_blank' ) {
event.preventDefault();
window.open( anchor.href, '_blank' ); // Open in new tab in parent
} else if ( anchor ) {
event.preventDefault();
window.location.href = anchor.href; // Navigate parent to new URL
}
}
} );
}

View File

@ -7,26 +7,29 @@ import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose'; import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withDispatch, withSelect } from '@wordpress/data'; import { withDispatch, withSelect } from '@wordpress/data';
import { PLUGINS_STORE_NAME } from '@woocommerce/data'; import { ONBOARDING_STORE_NAME } from '@woocommerce/data';
/**
* Button redirecting to Jetpack auth flow.
*
* Only render this component when the user has accepted Jetpack's Terms of Service.
* The API endpoint used by this component sets "jetpack_tos_agreed" to true when
* returning the URL.
*/
export class Connect extends Component { export class Connect extends Component {
constructor( props ) { constructor( props ) {
super( props ); super( props );
this.state = { this.state = {
isConnecting: false, isAwaitingRedirect: false,
isRedirecting: false,
}; };
this.connectJetpack = this.connectJetpack.bind( this ); this.connectJetpack = this.connectJetpack.bind( this );
props.setIsPending( true ); props.setIsPending( false );
} }
componentDidUpdate( prevProps ) { componentDidUpdate( prevProps ) {
const { createNotice, error, isRequesting, onError, setIsPending } = const { createNotice, error, onError, isRequesting } = this.props;
this.props;
if ( prevProps.isRequesting && ! isRequesting ) {
setIsPending( false );
}
if ( error && error !== prevProps.error ) { if ( error && error !== prevProps.error ) {
if ( onError ) { if ( onError ) {
@ -34,37 +37,35 @@ export class Connect extends Component {
} }
createNotice( 'error', error ); createNotice( 'error', error );
} }
if (
this.state.isAwaitingRedirect &&
! this.state.isRedirecting &&
! isRequesting &&
! error
) {
this.setState( { isRedirecting: true }, () => {
window.location = this.props.jetpackAuthUrl;
} );
}
} }
async connectJetpack() { connectJetpack() {
const { jetpackConnectUrl, onConnect } = this.props; const { onConnect } = this.props;
this.setState(
{
isConnecting: true,
},
() => {
if ( onConnect ) { if ( onConnect ) {
onConnect(); onConnect();
} }
window.location = jetpackConnectUrl;
} this.setState( { isAwaitingRedirect: true } );
);
} }
render() { render() {
const { const { error, onSkip, skipText, onAbort, abortText } = this.props;
hasErrors,
isRequesting,
onSkip,
skipText,
onAbort,
abortText,
} = this.props;
return ( return (
<Fragment> <Fragment>
{ hasErrors ? ( { error ? (
<Button <Button
isPrimary isPrimary
onClick={ () => window.location.reload() } onClick={ () => window.location.reload() }
@ -73,8 +74,7 @@ export class Connect extends Component {
</Button> </Button>
) : ( ) : (
<Button <Button
disabled={ isRequesting } isBusy={ this.state.isAwaitingRedirect }
isBusy={ this.state.isConnecting }
isPrimary isPrimary
onClick={ this.connectJetpack } onClick={ this.connectJetpack }
> >
@ -103,26 +103,24 @@ Connect.propTypes = {
createNotice: PropTypes.func.isRequired, createNotice: PropTypes.func.isRequired,
/** /**
* Human readable error message. * Human readable error message.
*
* Also used to determine if the "Retry" button should be displayed.
*/ */
error: PropTypes.string, error: PropTypes.string,
/**
* Bool to determine if the "Retry" button should be displayed.
*/
hasErrors: PropTypes.bool,
/** /**
* Bool to check if the connection URL is still being requested. * Bool to check if the connection URL is still being requested.
*/ */
isRequesting: PropTypes.bool, isRequesting: PropTypes.bool,
/** /**
* Generated Jetpack connection URL. * Generated Jetpack authentication URL.
*/ */
jetpackConnectUrl: PropTypes.string, jetpackAuthUrl: PropTypes.string,
/** /**
* Called before the redirect to Jetpack. * Called before the redirect to Jetpack.
*/ */
onConnect: PropTypes.func, onConnect: PropTypes.func,
/** /**
* Called when the plugin has an error retrieving the jetpackConnectUrl. * Called when the plugin has an error retrieving the jetpackAuthUrl.
*/ */
onError: PropTypes.func, onError: PropTypes.func,
/** /**
@ -157,20 +155,32 @@ Connect.defaultProps = {
export default compose( export default compose(
withSelect( ( select, props ) => { withSelect( ( select, props ) => {
const { getJetpackConnectUrl, isPluginsRequesting, getPluginsError } = const { getJetpackAuthUrl, isResolving } = select(
select( PLUGINS_STORE_NAME ); ONBOARDING_STORE_NAME
);
const queryArgs = { const queryArgs = {
redirect_url: props.redirectUrl || window.location.href, redirectUrl: props.redirectUrl || window.location.href,
from: 'woocommerce-services',
}; };
const isRequesting = isPluginsRequesting( 'getJetpackConnectUrl' );
const error = getPluginsError( 'getJetpackConnectUrl' ) || ''; const jetpackAuthUrlResponse = getJetpackAuthUrl( queryArgs );
const jetpackConnectUrl = getJetpackConnectUrl( queryArgs ); const isRequesting = isResolving( 'getJetpackAuthUrl', [ queryArgs ] );
let error;
if ( ! isResolving && ! jetpackAuthUrlResponse ) {
error = __( 'Error requesting connection URL.', 'woocommerce' );
}
if ( jetpackAuthUrlResponse?.errors?.length ) {
error = jetpackAuthUrlResponse?.errors[ 0 ];
}
return { return {
error, error,
isRequesting, isRequesting,
jetpackConnectUrl, jetpackAuthUrl: jetpackAuthUrlResponse.url,
}; };
} ), } ),
withDispatch( ( dispatch ) => { withDispatch( ( dispatch ) => {

View File

@ -46,9 +46,8 @@
} }
#wpbody { #wpbody {
display: block;
&.no-header { &.no-header {
display: block;
margin-top: 0; margin-top: 0;
.woocommerce-layout__primary { .woocommerce-layout__primary {

View File

@ -1,14 +1,6 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2483_9556)"> <path d="M70.8227 17.1683C73.8744 17.1683 77.2598 18.623 80 20.1745V13.4403C80 9.31419 77.664 7 73.5407 7H6.45562C2.33604 6.99628 0 9.31047 0 13.4366V47.3717C0 51.4978 2.33604 53.812 6.45933 53.812H29.5824C28.1214 56.4871 26.8014 59.7091 26.8014 62.626C26.8014 66.6553 28.1214 69.3676 30.7207 71.3916C32.9455 73.1254 35.9898 73.9662 39.9981 73.9662C44.0065 73.9662 47.0508 73.1254 49.2756 71.3916C51.8749 69.3676 53.1949 66.659 53.1949 62.626C53.1949 59.7053 51.8749 56.4871 50.4139 53.812H73.537C77.6603 53.812 79.9963 51.4978 79.9963 47.3717V40.645C77.2561 42.1964 73.8744 43.6512 70.819 43.6512C66.8032 43.6512 64.1001 42.3266 62.083 39.7185C60.355 37.4862 59.517 34.4316 59.517 30.4097C59.517 26.3878 60.355 23.3332 62.083 21.1009C64.1001 18.4928 66.7995 17.1683 70.819 17.1683H70.8227Z" fill="#F0F0F0"/>
<path d="M15.9776 43.968C11.1189 43.968 5.34584 44.544 2.95838 46.928C2.95838 46.928 2.95838 46.928 2.95439 46.932C2.95439 46.932 2.95439 46.932 2.95039 46.936C0.574906 49.332 -9.13044e-07 55.112 -7.00082e-07 59.984C-4.8712e-07 64.856 0.574907 70.64 2.95039 73.032C2.95039 73.032 2.95039 73.032 2.95439 73.036C2.95439 73.036 2.95438 73.036 2.95838 73.04C5.34584 75.424 11.1189 76 15.9776 76C20.8364 76 26.6094 75.424 28.9969 73.04C28.9969 73.04 28.9969 73.04 29.0009 73.036C29.0049 73.032 29.0009 73.036 29.0049 73.032C31.3804 70.636 31.9553 64.856 31.9553 59.984C31.9553 55.112 31.3804 49.328 29.0049 46.936C29.0049 46.936 29.0049 46.936 29.0009 46.932C28.9969 46.928 29.0009 46.932 28.9969 46.928C26.6094 44.544 20.8364 43.968 15.9776 43.968Z" fill="#757575"/> <path d="M39.9865 20.4834H6.6145V23.8321H39.9865V20.4834Z" fill="#DDDDDD"/>
<path d="M63.9776 3.968C59.1189 3.968 53.3458 4.544 50.9584 6.928C50.9584 6.928 50.9584 6.928 50.9544 6.932C50.9544 6.932 50.9544 6.932 50.9504 6.936C48.5749 9.332 48 15.112 48 19.984C48 24.856 48.5749 30.64 50.9504 33.032C50.9504 33.032 50.9504 33.032 50.9544 33.036C50.9544 33.036 50.9544 33.036 50.9584 33.04C53.3458 35.424 59.1189 36 63.9776 36C68.8364 36 74.6094 35.424 76.9969 33.04C76.9969 33.04 76.9969 33.04 77.0009 33.036C77.0049 33.032 77.0009 33.036 77.0049 33.032C79.3804 30.636 79.9553 24.856 79.9553 19.984C79.9553 15.112 79.3804 9.328 77.0049 6.936C77.0049 6.936 77.0049 6.936 77.0009 6.932C76.9969 6.928 77.0009 6.932 76.9969 6.92799C74.6094 4.544 68.8364 3.968 63.9776 3.968Z" fill="#757575"/> <path d="M39.9865 13.7158H6.6145V17.0645H39.9865V13.7158Z" fill="#DDDDDD"/>
<path d="M40 60C40 64.8656 40.7193 70.6467 43.6963 73.0375C43.6963 73.0375 43.6963 73.0375 43.7013 73.0415C43.7013 73.0415 43.7013 73.0415 43.7063 73.0455C46.6983 75.4243 53.9161 76 60 76C66.0839 76 73.3067 75.4243 76.2937 73.0455C76.2937 73.0455 76.2937 73.0455 76.2987 73.0415C76.2987 73.0415 76.2987 73.0415 76.3037 73.0375C79.2807 70.6467 80 64.8656 80 60C80 55.1344 79.2807 49.3533 76.3037 46.9625C76.3037 46.9625 76.3037 46.9625 76.2987 46.9585C76.2937 46.9545 76.2987 46.9585 76.2937 46.9545C73.3017 44.5757 66.0839 44 60 44C53.9161 44 46.6933 44.5757 43.7063 46.9545C43.7063 46.9545 43.7063 46.9545 43.7013 46.9585C43.6963 46.9625 43.7013 46.9585 43.6963 46.9625C40.7193 49.3533 40 55.1344 40 60Z" fill="#E0E0E0"/> <path d="M26.6377 27.1804H6.6145V30.5291H26.6377V27.1804Z" fill="#DDDDDD"/>
<path d="M-1.39876e-06 20C-9.73403e-07 24.8656 0.719276 30.6467 3.6963 33.0375C3.6963 33.0375 3.6963 33.0375 3.7013 33.0415C3.7013 33.0415 3.7013 33.0415 3.70629 33.0455C6.6983 35.4243 13.9161 36 20 36C26.0839 36 33.3067 35.4243 36.2937 33.0455C36.2937 33.0455 36.2937 33.0455 36.2987 33.0415C36.2987 33.0415 36.2987 33.0415 36.3037 33.0375C39.2807 30.6467 40 24.8656 40 20C40 15.1344 39.2807 9.35332 36.3037 6.96251C36.3037 6.96251 36.3037 6.96251 36.2987 6.95852C36.2937 6.95452 36.2987 6.95852 36.2937 6.95452C33.3017 4.57571 26.0839 4 20 4C13.9161 4 6.6933 4.57571 3.70629 6.95452C3.70629 6.95452 3.70629 6.95452 3.7013 6.95852C3.6963 6.96252 3.70129 6.95852 3.6963 6.96252C0.719274 9.35332 -1.82413e-06 15.1344 -1.39876e-06 20Z" fill="#E0E0E0"/>
<path d="M38.5095 14.0378C40.9729 11.6218 45.2004 11.3695 45.2004 10.4C45.2004 9.43044 40.9729 9.1782 38.5095 6.76221C36.0461 4.34621 35.789 0.199998 34.8004 0.199998C33.8118 0.199998 33.5546 4.34621 31.0913 6.76221C28.6279 9.1782 24.4004 9.43044 24.4004 10.4C24.4004 11.3695 28.6279 11.6218 31.0913 14.0378C33.5546 16.4538 33.8118 20.6 34.8004 20.6C35.789 20.6 36.0461 16.4538 38.5095 14.0378Z" fill="#757575"/>
</g>
<defs>
<clipPath id="clip0_2483_9556">
<rect width="80" height="80" fill="white" transform="translate(0 80) rotate(-90)"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,25 +1,17 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2584_6481)"> <g clip-path="url(#clip0_2293_37324)">
<path d="M79.9999 0H40V39.9999H79.9999V0Z" fill="#757575"/> <path d="M6.45466 7C2.33301 7 0 9.31262 0 13.4396V73.9767H59.8473L59.9917 13.4396C59.9917 9.31262 57.6587 7 53.537 7L6.45466 7Z" fill="#F0F0F0"/>
<path d="M71.4366 14.3426C74.7128 14.3426 77.3687 11.6867 77.3687 8.41043C77.3687 5.13419 74.7128 2.47827 71.4366 2.47827C68.1603 2.47827 65.5044 5.13419 65.5044 8.41043C65.5044 11.6867 68.1603 14.3426 71.4366 14.3426Z" fill="#E0E0E0"/> <path d="M80.0001 27.0774H43.3274V63.8897H80.0001V27.0774Z" fill="#DDDDDD"/>
<path d="M70.8753 24.5383C65.3379 24.5383 62.8872 32.6033 59.0083 32.6033C54.9361 32.6033 56.7333 15.4783 50.9219 15.4783C46.9533 15.4783 41.9711 26.1877 40 33.159V40H43.6031C43.626 39.927 45.908 26.8388 50.3351 26.9083C53.8943 26.9639 53.9435 36.1975 57.9296 36.6593C62.1283 37.1472 63.0067 31.0476 68.4492 31.0476C73.2785 31.0476 75.6994 40 75.6994 40H79.9999V35.3189C79.9999 33.4455 76.4126 24.54 70.877 24.54L70.8753 24.5383Z" fill="#E0E0E0"/> <path d="M36.6616 27.0774H6.66577V43.8086H36.6616V27.0774Z" fill="#DDDDDD"/>
<path d="M25.9218 50H4.07821C1.82588 50 0 51.8259 0 54.0782V75.9218C0 78.1741 1.82588 80 4.07821 80H25.9218C28.1741 80 30 78.1741 30 75.9218V54.0782C30 51.8259 28.1741 50 25.9218 50Z" fill="#E0E0E0"/> <path d="M36.6616 47.5229H6.66943V50.4974H36.6616V47.5229Z" fill="#DDDDDD"/>
<path d="M14.5919 69.7092L13.4895 66.928H7.28028L6.17781 69.7092H4.40088L9.43142 57.2632H11.3585L16.3711 69.7092H14.5942H14.5919ZM10.3837 58.8505L7.74635 65.5687H13.0391L10.3837 58.8505Z" fill="#757575"/> <path d="M23.3301 54.219H6.66577V57.1934H23.3301V54.219Z" fill="#DDDDDD"/>
<path d="M23.5888 69.7093V68.6831C22.8606 69.5036 21.8119 69.9328 20.5974 69.9328C19.0826 69.9328 17.4558 68.9066 17.4558 66.9482C17.4558 64.9897 19.0647 63.9815 20.5974 63.9815C21.832 63.9815 22.8606 64.3727 23.5888 65.1932V63.5701C23.5888 62.3762 22.6163 61.6854 21.3077 61.6854C20.2232 61.6854 19.3448 62.0588 18.5403 62.9352L17.886 61.9649C18.8585 60.9566 20.017 60.4714 21.4959 60.4714C23.423 60.4714 24.9938 61.3299 24.9938 63.512V69.707H23.5911L23.5888 69.7093ZM23.5888 67.8067V66.0897C23.0465 65.3609 22.092 64.9897 21.1195 64.9897C19.7907 64.9897 18.8742 65.8102 18.8742 66.9482C18.8742 68.0862 19.7907 68.9268 21.1195 68.9268C22.092 68.9268 23.0465 68.5534 23.5888 67.8067Z" fill="#757575"/> <path d="M60.1989 51.7429L59.3768 49.5232H53.785L52.9629 51.7429H49.8152L54.8404 38.6479H58.3214L63.3466 51.7429H60.1989ZM56.5809 41.4365L54.5479 47.073H58.614L56.5809 41.4365Z" fill="white"/>
<circle cx="15.7997" cy="9.52504" r="3.52504" transform="rotate(90 15.7997 9.52504)" fill="#E0E0E0"/> <path d="M70.179 51.7429V50.7427C69.5347 51.5272 68.42 51.9771 67.1869 51.9771C65.6834 51.9771 63.9207 50.9584 63.9207 48.8354C63.9207 46.5971 65.6834 45.7754 67.1869 45.7754C68.4608 45.7754 69.5532 46.1881 70.179 46.9317V45.7345C70.179 44.7716 69.3569 44.1432 68.1053 44.1432C67.1091 44.1432 66.1722 44.5373 65.3871 45.2623L64.4095 43.5149C65.5649 42.4738 67.0499 42.0239 68.5348 42.0239C70.7049 42.0239 72.6787 42.8865 72.6787 45.6156V51.7429H70.1753H70.179ZM70.179 49.4637V48.2888C69.768 47.7386 68.9866 47.4448 68.1867 47.4448C67.2091 47.4448 66.4055 47.9765 66.4055 48.88C66.4055 49.7835 67.2091 50.2928 68.1867 50.2928C68.9866 50.2928 69.768 50.0177 70.179 49.4674V49.4637Z" fill="white"/>
<circle cx="15.7997" cy="21.1237" r="3.52504" transform="rotate(90 15.7997 21.1237)" fill="#E0E0E0"/> <path d="M0 13.4396V16.9829H59.9917V13.4396C59.9917 9.31262 57.6587 7 53.537 7H6.45466C2.33301 7 0 9.31262 0 13.4396Z" fill="#DDDDDD"/>
<circle cx="15.7997" cy="32.7223" r="3.52504" transform="rotate(90 15.7997 32.7223)" fill="#757575"/>
<circle cx="15.7994" cy="32.7216" r="5.68555" transform="rotate(90 15.7994 32.7216)" stroke="#271B3D" stroke-width="0.227422"/>
<path d="M75.001 80L75.001 50L70.001 50L70.001 80L75.001 80Z" fill="#757575"/>
<path d="M62.001 80L62.001 50L57.001 50L57.001 80L62.001 80Z" fill="#757575"/>
<path d="M49.0002 80L49.0002 50L44.0002 50L44.0002 80L49.0002 80Z" fill="#757575"/>
<path d="M76.3637 69C74.9082 69 74.4716 70.0689 72.7182 70.0689C70.9647 70.0689 70.5281 69 69.0726 69C67.5125 69 67.0008 70.3509 67.0008 71.9929C67.0008 73.6349 67.5125 74.9857 69.0726 74.9857C70.5281 74.9857 70.9647 73.9169 72.7182 73.9169C74.4716 73.9169 74.9082 74.9857 76.3637 74.9857C77.9238 74.9857 78.4355 73.6349 78.4355 71.9929C78.4355 70.3509 77.9238 69 76.3637 69Z" fill="#E0E0E0"/>
<path d="M63.3637 58C61.9082 58 61.4716 59.0689 59.7182 59.0689C57.9647 59.0689 57.5281 58 56.0726 58C54.5125 58 54.0008 59.3509 54.0008 60.9929C54.0008 62.6349 54.5125 63.9857 56.0726 63.9857C57.5281 63.9857 57.9647 62.9169 59.7182 62.9169C61.4716 62.9169 61.9082 63.9857 63.3637 63.9857C64.9238 63.9857 65.4355 62.6349 65.4355 60.9929C65.4355 59.3509 64.9238 58 63.3637 58Z" fill="#E0E0E0"/>
<path d="M50.363 66C48.9075 66 48.4709 67.0689 46.7174 67.0689C44.964 67.0689 44.5274 66 43.0719 66C41.5117 66 41 67.3509 41 68.9929C41 70.6349 41.5117 71.9857 43.0719 71.9857C44.5274 71.9857 44.964 70.9169 46.7174 70.9169C48.4709 70.9169 48.9075 71.9857 50.363 71.9857C51.9231 71.9857 52.4348 70.6349 52.4348 68.9929C52.4348 67.3509 51.9231 66 50.363 66Z" fill="#E0E0E0"/>
</g> </g>
<defs> <defs>
<clipPath id="clip0_2584_6481"> <clipPath id="clip0_2293_37324">
<rect width="80" height="80" fill="white"/> <rect width="80" height="66.9767" fill="white" transform="translate(0 7)"/>
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -15,6 +15,10 @@ import Products from '../products/products';
import SearchResults from '../search-results/search-results'; import SearchResults from '../search-results/search-results';
import { MarketplaceContext } from '../../contexts/marketplace-context'; import { MarketplaceContext } from '../../contexts/marketplace-context';
import { fetchSearchResults } from '../../utils/functions'; import { fetchSearchResults } from '../../utils/functions';
import {
recordMarketplaceView,
recordLegacyTabView,
} from '../../utils/tracking';
export default function Content(): JSX.Element { export default function Content(): JSX.Element {
const marketplaceContextValue = useContext( MarketplaceContext ); const marketplaceContextValue = useContext( MarketplaceContext );
@ -25,7 +29,19 @@ export default function Content(): JSX.Element {
// Get the content for this screen // Get the content for this screen
useEffect( () => { useEffect( () => {
const abortController = new AbortController(); const abortController = new AbortController();
if ( [ '', 'discover' ].includes( selectedTab ) ) { // we are recording both the new and legacy events here for now
// they're separate methods to make it easier to remove the legacy one later
const marketplaceViewProps = {
view: query?.tab,
search_term: query?.term,
product_type: query?.section,
category: query?.category,
};
recordMarketplaceView( marketplaceViewProps );
recordLegacyTabView( marketplaceViewProps );
if ( query.tab && [ '', 'discover' ].includes( query.tab ) ) {
return; return;
} }
@ -43,9 +59,9 @@ export default function Content(): JSX.Element {
'category', 'category',
query.category === '_all' ? '' : query.category query.category === '_all' ? '' : query.category
); );
} else if ( selectedTab === 'themes' ) { } else if ( query?.tab === 'themes' ) {
params.append( 'category', 'themes' ); params.append( 'category', 'themes' );
} else if ( selectedTab === 'search' ) { } else if ( query?.tab === 'search' ) {
params.append( 'category', 'extensions-themes' ); params.append( 'category', 'extensions-themes' );
} }
@ -67,7 +83,13 @@ export default function Content(): JSX.Element {
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, [ query.term, query.category, selectedTab, setIsLoading ] ); }, [
query.term,
query.category,
query?.tab,
setIsLoading,
query?.section,
] );
const renderContent = (): JSX.Element => { const renderContent = (): JSX.Element => {
switch ( selectedTab ) { switch ( selectedTab ) {

View File

@ -185,6 +185,7 @@
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
} }
.woocommerce-marketplace__product-card__image { .woocommerce-marketplace__product-card__image {
border-bottom: 1px solid $gray-200;
grid-column-start: span 2; grid-column-start: span 2;
overflow: hidden; overflow: hidden;
padding-top: 75%; padding-top: 75%;

View File

@ -11,6 +11,11 @@
.woocommerce-marketplace__no-results__product-group { .woocommerce-marketplace__no-results__product-group {
margin-top: $grid-unit-60; margin-top: $grid-unit-60;
.woocommerce-marketplace__product-list-title {
font-size: 16px;
font-weight: 600;
}
} }
.woocommerce-marketplace__no-results__icon { .woocommerce-marketplace__no-results__icon {

View File

@ -24,10 +24,12 @@
.woocommerce-store-alerts { .woocommerce-store-alerts {
margin-left: 16px; margin-left: 16px;
margin-right: 16px; margin-right: 16px;
margin-top: 16px;
@media (min-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
margin-left: 32px; margin-left: 32px;
margin-right: 32px; margin-right: 32px;
margin-top: 32px;
} }
} }
} }

View File

@ -0,0 +1,93 @@
/**
* External dependencies
*/
import { recordEvent } from '@woocommerce/tracks';
interface MarketplaceViewProps {
view?: string;
search_term?: string;
product_type?: string;
category?: string;
}
/**
* Record a marketplace view event.
* This is a new event that is easier to understand and implement consistently
*/
function recordMarketplaceView( props: MarketplaceViewProps ) {
// The category prop changes to a blank string on first viewing all products after a search.
// This is undesirable and causes a duplicate event that will artificially inflate event counts.
if ( props.category === '' ) {
return;
}
const view = props.view || 'discover';
const search_term = props.search_term || null;
const product_type = props.product_type || null;
const category = props.category || null;
const eventProps = {
...( view && { view } ),
...( search_term && { search_term } ),
...( product_type && { product_type } ),
...( category && { category } ),
};
// User sees the default extensions or themes view
if ( view && [ 'extensions', 'themes' ].includes( view ) && ! category ) {
eventProps.category = '_all';
}
// User clicks the `View All` button on search results
if ( view && view === 'search' && product_type && ! category ) {
eventProps.category = '_all';
}
recordEvent( 'marketplace_view', eventProps );
}
/**
* Ensure we still have legacy events in place
* the "view" prop maps to a "section" prop in the event for compatibility with old funnels.
*
* @param props The props object containing view, search_term, section, and category.
*/
function recordLegacyTabView( props: MarketplaceViewProps ) {
// product_type will artificially inflate legacy event counts.
if ( props.product_type ) {
return;
}
let oldEventName = 'extensions_view';
const view = props.view || '_featured';
const search_term = props.search_term || null;
const category = props.category || null;
const oldEventProps = {
// legacy event refers to "section" instead of "view"
...( view && { section: view } ),
...( search_term && { search_term } ),
version: '2',
};
switch ( view ) {
case 'extensions':
oldEventProps.section = category || '_all';
break;
case 'themes':
oldEventProps.section = 'themes';
break;
case 'search':
oldEventName = 'extensions_view_search';
oldEventProps.section = view;
oldEventProps.search_term = search_term || '';
break;
case 'my-subscriptions':
oldEventName = 'subscriptions_view';
oldEventProps.section = 'helper';
break;
}
recordEvent( oldEventName, oldEventProps );
}
export { recordMarketplaceView, recordLegacyTabView };

View File

@ -31,7 +31,7 @@ const WooCommerceServicesItem: React.FC< {
actions.push( { actions.push( {
url: getAdminLink( 'plugins.php' ), url: getAdminLink( 'plugins.php' ),
label: __( label: __(
'Finish the setup by connecting your store to Jetpack.', 'Finish the setup by connecting your store to WordPress.com.',
'woocommerce' 'woocommerce'
), ),
} ); } );

View File

@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { Text } from '@woocommerce/experimental';
import interpolateComponents from '@automattic/interpolate-components';
import { __, sprintf } from '@wordpress/i18n';
import { Link } from '@woocommerce/components';
export const TermsOfService = ( { buttonText } ) => (
<Text
variant="caption"
className="woocommerce-task__caption is-tos"
size="12"
lineHeight="16px"
style={ { display: 'block' } }
>
{ interpolateComponents( {
mixedString: sprintf(
__(
'By clicking "%s," you agree to our {{tosLink}}Terms of Service{{/tosLink}} and have read our {{privacyPolicyLink}}Privacy Policy{{/privacyPolicyLink}}.',
'woocommerce'
),
buttonText
),
components: {
tosLink: (
<Link
href={ 'https://wordpress.com/tos/' }
target="_blank"
type="external"
>
<></>
</Link>
),
privacyPolicyLink: (
<Link
href={ 'https://automattic.com/privacy/' }
target="_blank"
type="external"
>
<></>
</Link>
),
},
} ) }
</Text>
);

View File

@ -1,31 +0,0 @@
/**
* External dependencies
*/
import { registerPlugin } from '@wordpress/plugins';
import { WooOnboardingTaskListItem } from '@woocommerce/onboarding';
import { getAdminLink } from '@woocommerce/settings';
const CustomizeStoreTaskItem = () => (
<WooOnboardingTaskListItem id="customize-store">
{ ( {
defaultTaskItem: DefaultTaskItem,
}: {
defaultTaskItem: ( props: { onClick: () => void } ) => JSX.Element;
} ) => (
<DefaultTaskItem
onClick={ () => {
// We need to use window.location.href instead of navigateTo because we need to initiate a full page refresh to ensure that all dependencies are loaded.
window.location.href = getAdminLink(
'admin.php?page=wc-admin&path=%2Fcustomize-store'
);
} }
/>
) }
</WooOnboardingTaskListItem>
);
registerPlugin( 'woocommerce-admin-task-customize-store', {
// @ts-expect-error scope is not defined in the type definition but it is a valid property
scope: 'woocommerce-tasks',
render: CustomizeStoreTaskItem,
} );

View File

@ -9,7 +9,7 @@ import { recordEvent } from '@woocommerce/tracks';
import { default as ConnectForm } from '~/dashboard/components/connect'; import { default as ConnectForm } from '~/dashboard/components/connect';
type ConnectProps = { type ConnectProps = {
onConnect: () => void; onConnect?: () => void;
}; };
export const Connect: React.FC< ConnectProps > = ( { onConnect } ) => { export const Connect: React.FC< ConnectProps > = ( { onConnect } ) => {

View File

@ -2,11 +2,9 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import interpolateComponents from '@automattic/interpolate-components'; import { Plugins as PluginInstaller } from '@woocommerce/components';
import { Link, Plugins as PluginInstaller } from '@woocommerce/components';
import { OPTIONS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data'; import { OPTIONS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { Text } from '@woocommerce/experimental';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element'; import { useEffect } from '@wordpress/element';
@ -14,6 +12,7 @@ import { useEffect } from '@wordpress/element';
* Internal dependencies * Internal dependencies
*/ */
import { createNoticesFromResponse } from '~/lib/notices'; import { createNoticesFromResponse } from '~/lib/notices';
import { TermsOfService } from '~/task-lists/components/terms-of-service';
const isWcConnectOptions = ( const isWcConnectOptions = (
wcConnectOptions: unknown wcConnectOptions: unknown
@ -58,15 +57,6 @@ export const Plugins: React.FC< Props > = ( {
nextStep(); nextStep();
}, [ nextStep, pluginsToActivate, tosAccepted ] ); }, [ nextStep, pluginsToActivate, tosAccepted ] );
const agreementText = pluginsToActivate.includes( 'woocommerce-services' )
? __(
'By installing Jetpack and WooCommerce Shipping you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
)
: __(
'By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
);
if ( isResolving ) { if ( isResolving ) {
return null; return null;
@ -74,6 +64,11 @@ export const Plugins: React.FC< Props > = ( {
return ( return (
<> <>
{ ! tosAccepted && (
<TermsOfService
buttonText={ __( 'Install & enable', 'woocommerce' ) }
/>
) }
<PluginInstaller <PluginInstaller
onComplete={ ( onComplete={ (
activatedPlugins: string[], activatedPlugins: string[],
@ -96,30 +91,6 @@ export const Plugins: React.FC< Props > = ( {
} }
pluginSlugs={ pluginsToActivate } pluginSlugs={ pluginsToActivate }
/> />
{ ! tosAccepted && (
<Text
variant="caption"
className="woocommerce-task__caption"
size="12"
lineHeight="16px"
style={ { display: 'block' } }
>
{ interpolateComponents( {
mixedString: agreementText,
components: {
link: (
<Link
href={ 'https://wordpress.com/tos/' }
target="_blank"
type="external"
>
<></>
</Link>
),
},
} ) }
</Text>
) }
</> </>
); );
}; };

View File

@ -20,7 +20,7 @@ import { redirectToWCSSettings } from './utils';
/** /**
* Plugins required to automate shipping. * Plugins required to automate shipping.
*/ */
const AUTOMATION_PLUGINS = [ 'jetpack', 'woocommerce-services' ]; const AUTOMATION_PLUGINS = [ 'woocommerce-services' ];
export const ShippingRecommendation: React.FC< export const ShippingRecommendation: React.FC<
TaskProps & ShippingRecommendationProps TaskProps & ShippingRecommendationProps
@ -94,12 +94,7 @@ export const ShippingRecommendation: React.FC<
}, },
{ {
key: 'plugins', key: 'plugins',
label: pluginsToActivate.includes( 'woocommerce-services' ) label: __( 'Install WooCommerce Shipping', 'woocommerce' ),
? __(
'Install Jetpack and WooCommerce Shipping',
'woocommerce'
)
: __( 'Install Jetpack', 'woocommerce' ),
description: __( description: __(
'Enable shipping label printing and discounted rates', 'Enable shipping label printing and discounted rates',
'woocommerce' 'woocommerce'
@ -126,7 +121,7 @@ export const ShippingRecommendation: React.FC<
{ __( 'Complete task', 'woocommerce' ) } { __( 'Complete task', 'woocommerce' ) }
</Button> </Button>
) : ( ) : (
<Connect onConnect={ redirect } /> <Connect />
), ),
}, },
]; ];

View File

@ -60,7 +60,20 @@ const ShippingRecommendation = ( props: ShippingRecommendationProps ) => {
}; };
describe( 'ShippingRecommendation', () => { describe( 'ShippingRecommendation', () => {
test( 'should show plugins step when jetpack is not installed and activated', () => { test( 'should show plugins step when woocommerce-services is not installed and activated', () => {
const { getByRole } = render(
<ShippingRecommendation
isJetpackConnected={ false }
isResolving={ false }
activePlugins={ [ 'foo' ] }
/>
);
expect(
getByRole( 'button', { name: 'Install & enable' } )
).toBeInTheDocument();
} );
test( 'should show connect step when WCS&T is activated but not yet connected', () => {
const { getByRole } = render( const { getByRole } = render(
<ShippingRecommendation <ShippingRecommendation
isJetpackConnected={ false } isJetpackConnected={ false }
@ -68,43 +81,17 @@ describe( 'ShippingRecommendation', () => {
activePlugins={ [ 'woocommerce-services' ] } activePlugins={ [ 'woocommerce-services' ] }
/> />
); );
expect(
getByRole( 'button', { name: 'Install & enable' } )
).toBeInTheDocument();
} );
test( 'should show plugins step when woocommerce-services is not installed and activated', () => {
const { getByRole } = render(
<ShippingRecommendation
isJetpackConnected={ false }
isResolving={ false }
activePlugins={ [ 'jetpack' ] }
/>
);
expect(
getByRole( 'button', { name: 'Install & enable' } )
).toBeInTheDocument();
} );
test( 'should show connect step when both plugins are activated', () => {
const { getByRole } = render(
<ShippingRecommendation
isJetpackConnected={ false }
isResolving={ false }
activePlugins={ [ 'jetpack', 'woocommerce-services' ] }
/>
);
expect( expect(
getByRole( 'button', { name: 'Connect' } ) getByRole( 'button', { name: 'Connect' } )
).toBeInTheDocument(); ).toBeInTheDocument();
} ); } );
test( 'should show "complete task" button when both plugins are activated and jetpack is connected', () => { test( 'should show "complete task" button when WCS&T is activated and Jetpack is connected', () => {
const { getByRole } = render( const { getByRole } = render(
<ShippingRecommendation <ShippingRecommendation
isJetpackConnected={ true } isJetpackConnected={ true }
isResolving={ false } isResolving={ false }
activePlugins={ [ 'jetpack', 'woocommerce-services' ] } activePlugins={ [ 'woocommerce-services' ] }
/> />
); );
expect( expect(
@ -117,7 +104,7 @@ describe( 'ShippingRecommendation', () => {
<ShippingRecommendation <ShippingRecommendation
isJetpackConnected={ true } isJetpackConnected={ true }
isResolving={ false } isResolving={ false }
activePlugins={ [ 'jetpack', 'woocommerce-services' ] } activePlugins={ [ 'woocommerce-services' ] }
/> />
); );

View File

@ -9,7 +9,6 @@ import './appearance';
import './tax'; import './tax';
import './woocommerce-payments'; import './woocommerce-payments';
import './deprecated-tasks'; import './deprecated-tasks';
import './customize-store-tasklist-item';
const possiblyImportProductTask = async () => { const possiblyImportProductTask = async () => {
if ( isImportProduct() ) { if ( isImportProduct() ) {

View File

@ -16,7 +16,7 @@ import { createNoticesFromResponse } from '../../../lib/notices';
import { getAdminSetting } from '~/utils/admin-settings'; import { getAdminSetting } from '~/utils/admin-settings';
const EXPERIMENT_NAME = const EXPERIMENT_NAME =
'woocommerce_product_creation_experience_add_variations_202310_v1'; 'woocommerce_product_creation_experience_add_variations_202310_v2';
export const useCreateProductByType = () => { export const useCreateProductByType = () => {
const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME ); const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME );

View File

@ -22,7 +22,6 @@ import {
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { registerPlugin } from '@wordpress/plugins'; import { registerPlugin } from '@wordpress/plugins';
import { WooOnboardingTask } from '@woocommerce/onboarding'; import { WooOnboardingTask } from '@woocommerce/onboarding';
import { Text } from '@woocommerce/experimental';
import classNames from 'classnames'; import classNames from 'classnames';
/** /**
@ -38,6 +37,7 @@ import {
ShippingLayoutColumn, ShippingLayoutColumn,
ShippingLayoutRow, ShippingLayoutRow,
} from './shipping-providers/partners'; } from './shipping-providers/partners';
import { TermsOfService } from '~/task-lists/components/terms-of-service';
export class Shipping extends Component { export class Shipping extends Component {
constructor( props ) { constructor( props ) {
@ -60,6 +60,8 @@ export class Shipping extends Component {
this.storeLocationCompleted = false; this.storeLocationCompleted = false;
this.shippingPartners = props.shippingPartners; this.shippingPartners = props.shippingPartners;
this.jetpackAuthRedirectUrl = getAdminLink( 'admin.php?page=wc-admin' );
} }
componentDidMount() { componentDidMount() {
@ -207,11 +209,6 @@ export class Shipping extends Component {
return pluginToPromote.slug; return pluginToPromote.slug;
} ); } );
// Add jetpack to the list if the list includes woocommerce-services
if ( pluginsToActivate.includes( 'woocommerce-services' ) ) {
pluginsToActivate.push( 'jetpack' );
}
const onShippingPluginInstalltionSkip = () => { const onShippingPluginInstalltionSkip = () => {
recordEvent( 'tasklist_shipping_label_printing', { recordEvent( 'tasklist_shipping_label_printing', {
install: false, install: false,
@ -331,29 +328,48 @@ export class Shipping extends Component {
'woocommerce' 'woocommerce'
), ),
content: ( content: (
<>
{ ! isJetpackConnected &&
pluginsToActivate.includes(
'woocommerce-services'
) && (
<TermsOfService
buttonText={ __(
'Install & enable',
'woocommerce'
) }
/>
) }
<Plugins <Plugins
onComplete={ ( _plugins, response ) => { onComplete={ ( _plugins, response ) => {
createNoticesFromResponse( response ); createNoticesFromResponse( response );
recordEvent( 'tasklist_shipping_label_printing', { recordEvent(
'tasklist_shipping_label_printing',
{
install: true, install: true,
plugins_to_activate: pluginsToActivate, plugins_to_activate: pluginsToActivate,
} ); }
);
this.completeStep(); this.completeStep();
} } } }
onError={ ( errors, response ) => onError={ ( errors, response ) =>
createNoticesFromResponse( response ) createNoticesFromResponse( response )
} }
onSkip={ () => { onSkip={ () => {
recordEvent( 'tasklist_shipping_label_printing', { recordEvent(
'tasklist_shipping_label_printing',
{
install: false, install: false,
plugins_to_activate: pluginsToActivate, plugins_to_activate: pluginsToActivate,
} ); }
);
invalidateResolutionForStoreSelector(); invalidateResolutionForStoreSelector();
getHistory().push( getNewPath( {}, '/', {} ) ); getHistory().push( getNewPath( {}, '/', {} ) );
onComplete(); onComplete();
} } } }
pluginSlugs={ pluginsToActivate } pluginSlugs={ pluginsToActivate }
/> />
</>
), ),
visible: pluginsToActivate.length, visible: pluginsToActivate.length,
}, },
@ -368,9 +384,7 @@ export class Shipping extends Component {
), ),
content: ( content: (
<Connect <Connect
redirectUrl={ getAdminLink( redirectUrl={ this.jetpackAuthRedirectUrl }
'admin.php?page=wc-admin'
) }
completeStep={ this.completeStep } completeStep={ this.completeStep }
onConnect={ () => { onConnect={ () => {
recordEvent( 'tasklist_shipping_connect_store' ); recordEvent( 'tasklist_shipping_connect_store' );
@ -383,17 +397,6 @@ export class Shipping extends Component {
// Override the step fields for the smart shipping defaults. // Override the step fields for the smart shipping defaults.
if ( this.shippingSmartDefaultsEnabled ) { if ( this.shippingSmartDefaultsEnabled ) {
const agreementText = pluginsToActivate.includes(
'woocommerce-services'
)
? __(
'By installing Jetpack and WooCommerce Shipping you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
)
: __(
'By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
);
const shippingSmartDefaultsSteps = { const shippingSmartDefaultsSteps = {
rates: { rates: {
label: __( 'Review your shipping options', 'woocommerce' ), label: __( 'Review your shipping options', 'woocommerce' ),
@ -538,9 +541,22 @@ export class Shipping extends Component {
) } ) }
{ pluginsToPromote.length === 1 && { pluginsToPromote.length === 1 &&
pluginsToPromote[ 0 ].slug ? ( pluginsToPromote[ 0 ].slug ? (
<>
{ ! isJetpackConnected &&
pluginsToPromote[ 0 ].slug ===
'woocommerce-services' && (
<TermsOfService
buttonText={ __(
'Install and enable',
'woocommerce'
) }
/>
) }
<Plugins <Plugins
onComplete={ ( _plugins, response ) => { onComplete={ ( _plugins, response ) => {
createNoticesFromResponse( response ); createNoticesFromResponse(
response
);
recordEvent( recordEvent(
'tasklist_shipping_label_printing', 'tasklist_shipping_label_printing',
{ {
@ -553,15 +569,20 @@ export class Shipping extends Component {
this.completeStep(); this.completeStep();
} } } }
onError={ ( errors, response ) => onError={ ( errors, response ) =>
createNoticesFromResponse( response ) createNoticesFromResponse(
response
)
}
onSkip={
onShippingPluginInstalltionSkip
} }
onSkip={ onShippingPluginInstalltionSkip }
pluginSlugs={ pluginsToActivate } pluginSlugs={ pluginsToActivate }
installText={ __( installText={ __(
'Install and enable', 'Install and enable',
'woocommerce' 'woocommerce'
) } ) }
/> />
</>
) : ( ) : (
<Button <Button
isTertiary isTertiary
@ -576,36 +597,6 @@ export class Shipping extends Component {
{ __( 'No Thanks', 'woocommerce' ) } { __( 'No Thanks', 'woocommerce' ) }
</Button> </Button>
) } ) }
{ ! isJetpackConnected &&
pluginsToActivate.includes(
'woocommerce-services'
) && (
<Text
variant="caption"
className="woocommerce-task__caption"
size="12"
lineHeight="16px"
style={ { display: 'block' } }
>
{ interpolateComponents( {
mixedString: agreementText,
components: {
link: (
<Link
href={
'https://wordpress.com/tos/'
}
target="_blank"
type="external"
>
<></>
</Link>
),
},
} ) }
</Text>
) }
</> </>
), ),
}, },

View File

@ -7,7 +7,7 @@ import { TaskType } from '@woocommerce/data';
/** /**
* Plugins required to automate taxes. * Plugins required to automate taxes.
*/ */
export const AUTOMATION_PLUGINS = [ 'jetpack', 'woocommerce-services' ]; export const AUTOMATION_PLUGINS = [ 'woocommerce-services' ];
/** /**
* Check if a store has a complete address given general settings. * Check if a store has a complete address given general settings.

View File

@ -33,8 +33,7 @@ export const AutomatedTaxes: React.FC<
<p> <p>
{ interpolateComponents( { { interpolateComponents( {
mixedString: __( mixedString: __(
'{{strong}}Jetpack{{/strong}} and {{strong}}WooCommerce Tax{{/strong}} ' + '{{strong}}WooCommerce Tax{{/strong}} can automate your sales tax calculations for you.',
'can automate your sales tax calculations for you.',
'woocommerce' 'woocommerce'
), ),
components: { components: {

View File

@ -3,7 +3,6 @@
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import interpolateComponents from '@automattic/interpolate-components'; import interpolateComponents from '@automattic/interpolate-components';
import { Link } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { updateQueryString } from '@woocommerce/navigation'; import { updateQueryString } from '@woocommerce/navigation';
@ -13,6 +12,7 @@ import { updateQueryString } from '@woocommerce/navigation';
import { PartnerCard } from '../components/partner-card'; import { PartnerCard } from '../components/partner-card';
import logo from './logo.png'; import logo from './logo.png';
import { TaxChildProps } from '../utils'; import { TaxChildProps } from '../utils';
import { TermsOfService } from '~/task-lists/components/terms-of-service';
export const Card: React.FC< TaxChildProps > = () => { export const Card: React.FC< TaxChildProps > = () => {
return ( return (
@ -31,43 +31,14 @@ export const Card: React.FC< TaxChildProps > = () => {
strong: <strong />, strong: <strong />,
}, },
} ), } ),
interpolateComponents( {
mixedString: __(
'Powered by {{link}}Jetpack{{/link}}',
'woocommerce'
),
components: {
link: (
<Link
type="external"
href="https://woocommerce.com/products/jetpack/?utm_medium=product"
target="_blank"
>
<></>
</Link>
),
},
} ),
// eslint-disable-next-line @wordpress/i18n-translator-comments // eslint-disable-next-line @wordpress/i18n-translator-comments
__( '100% free', 'woocommerce' ), __( '100% free', 'woocommerce' ),
] } ] }
terms={ interpolateComponents( { terms={
mixedString: __( <TermsOfService
'By installing WooCommerce Tax and Jetpack you agree to the {{link}}Terms of Service{{/link}}.', buttonText={ __( 'Continue setup', 'woocommerce' ) }
'woocommerce' />
), }
components: {
link: (
<Link
href={ 'https://wordpress.com/tos/' }
target="_blank"
type="external"
>
<></>
</Link>
),
},
} ) }
actionText={ __( 'Continue setup', 'woocommerce' ) } actionText={ __( 'Continue setup', 'woocommerce' ) }
onClick={ () => { onClick={ () => {
recordEvent( 'tasklist_tax_select_option', { recordEvent( 'tasklist_tax_select_option', {

View File

@ -2,11 +2,9 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import interpolateComponents from '@automattic/interpolate-components'; import { Plugins as PluginInstaller } from '@woocommerce/components';
import { Link, Plugins as PluginInstaller } from '@woocommerce/components';
import { OPTIONS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data'; import { OPTIONS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data';
import { recordEvent, queueRecordEvent } from '@woocommerce/tracks'; import { recordEvent, queueRecordEvent } from '@woocommerce/tracks';
import { Text } from '@woocommerce/experimental';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element'; import { useEffect } from '@wordpress/element';
@ -15,6 +13,7 @@ import { useEffect } from '@wordpress/element';
*/ */
import { createNoticesFromResponse } from '~/lib/notices'; import { createNoticesFromResponse } from '~/lib/notices';
import { SetupStepProps } from './setup'; import { SetupStepProps } from './setup';
import { TermsOfService } from '~/task-lists/components/terms-of-service';
const isWcConnectOptions = ( const isWcConnectOptions = (
wcConnectOptions: unknown wcConnectOptions: unknown
@ -57,22 +56,17 @@ export const Plugins: React.FC< SetupStepProps > = ( {
nextStep(); nextStep();
}, [ isResolving ] ); }, [ isResolving ] );
const agreementText = pluginsToActivate.includes( 'woocommerce-services' )
? __(
'By installing Jetpack and WooCommerce Tax you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
)
: __(
'By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce'
);
if ( isResolving ) { if ( isResolving ) {
return null; return null;
} }
return ( return (
<> <>
{ ! tosAccepted && (
<TermsOfService
buttonText={ __( 'Install & enable', 'woocommerce' ) }
/>
) }
<PluginInstaller <PluginInstaller
onComplete={ ( onComplete={ (
activatedPlugins: string[], activatedPlugins: string[],
@ -99,30 +93,8 @@ export const Plugins: React.FC< SetupStepProps > = ( {
skipText={ __( 'Set up manually', 'woocommerce' ) } skipText={ __( 'Set up manually', 'woocommerce' ) }
onAbort={ () => onDisable() } onAbort={ () => onDisable() }
abortText={ __( "I don't charge sales tax", 'woocommerce' ) } abortText={ __( "I don't charge sales tax", 'woocommerce' ) }
pluginSlugs={ pluginsToActivate }
/> />
{ ! tosAccepted && (
<Text
variant="caption"
className="woocommerce-task__caption"
size="12"
lineHeight="16px"
>
{ interpolateComponents( {
mixedString: agreementText,
components: {
link: (
<Link
href={ 'https://wordpress.com/tos/' }
target="_blank"
type="external"
>
<></>
</Link>
),
},
} ) }
</Text>
) }
</> </>
); );
}; };

View File

@ -103,11 +103,9 @@ export const Setup: React.FC< SetupProps > = ( {
}, },
{ {
key: 'plugins', key: 'plugins',
label: pluginsToActivate.includes( 'woocommerce-services' ) label: __( 'Install WooCommerce Tax', 'woocommerce' ),
? __( 'Install Jetpack and WooCommerce Tax', 'woocommerce' )
: __( 'Install Jetpack', 'woocommerce' ),
description: __( description: __(
'Jetpack and WooCommerce Tax allow you to automate sales tax calculations', 'WooCommerce Tax allows you to automate sales tax calculations',
'woocommerce' 'woocommerce'
), ),
content: <Plugins { ...stepProps } />, content: <Plugins { ...stepProps } />,

View File

@ -4,7 +4,6 @@
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { TaskType } from '@woocommerce/data'; import { TaskType } from '@woocommerce/data';
import { getAdminLink } from '@woocommerce/settings';
/** /**
* Internal dependencies * Internal dependencies
@ -13,6 +12,7 @@ import { WC_ASSET_URL } from '../../../../utils/admin-settings';
const CustomizeStoreHeader = ( { const CustomizeStoreHeader = ( {
task, task,
goToTask,
}: { }: {
task: TaskType; task: TaskType;
goToTask: React.MouseEventHandler; goToTask: React.MouseEventHandler;
@ -40,12 +40,7 @@ const CustomizeStoreHeader = ( {
<Button <Button
isSecondary={ task.isComplete } isSecondary={ task.isComplete }
isPrimary={ ! task.isComplete } isPrimary={ ! task.isComplete }
onClick={ () => { onClick={ goToTask }
// We need to use window.location.href instead of navigateTo because we need to initiate a full page refresh to ensure that all dependencies are loaded.
window.location.href = getAdminLink(
'admin.php?page=wc-admin&path=%2Fcustomize-store'
);
} }
> >
{ __( 'Start customizing', 'woocommerce' ) } { __( 'Start customizing', 'woocommerce' ) }
</Button> </Button>

View File

@ -22,7 +22,7 @@ const TaxHeader = ( { task, goToTask } ) => {
<h1>{ __( 'Add your tax rates', 'woocommerce' ) }</h1> <h1>{ __( 'Add your tax rates', 'woocommerce' ) }</h1>
<p> <p>
{ __( { __(
'Set up tax rates manually or use WooCommerce and Jetpack to automate your sales tax calculations for you.', 'Set up tax rates manually or use WooCommerce Tax to automate your sales tax calculations for you.',
'woocommerce' 'woocommerce'
) } ) }
</p> </p>

View File

@ -238,6 +238,10 @@
margin-top: $gap; margin-top: $gap;
} }
.woocommerce-task-dashboard__container .woocommerce-task__caption.is-tos {
margin-bottom: $gap;
}
.woocommerce-task-list__setup { .woocommerce-task-list__setup {
.woocommerce-experimental-list .woocommerce-experimental-list
.woocommerce-experimental-list__item.complete { .woocommerce-experimental-list__item.complete {

View File

@ -72,20 +72,20 @@ Both of these endpoints use WooCommerce Core's `WC_Helper_API` directly. The mai
To disconnect from WooCommerce.com, go to `WooCommerce > Extensions > WooCommerce.com Subscriptions > Connected to WooCommerce.com > Disconnect`. To disconnect from WooCommerce.com, go to `WooCommerce > Extensions > WooCommerce.com Subscriptions > Connected to WooCommerce.com > Disconnect`.
## Jetpack Connection ## WordPress.com Connection
Using Jetpack & WooCommerce Shipping & Tax allows us to offer additional features to new WooCommerce users as well as simplify parts of the setup process. For example, we can do automated tax calculations for certain countries, significantly simplifying the tax task. To make this work, the user needs to be connected to a WordPress.com account. This also means development and testing of these features needs to be done on a Jetpack connected site. Search the MGS & the Field Guide for additional resources on testing Jetpack with local setups. Using a WordPress.com connection in WooCommerce Shipping & Tax allows us to offer additional features to new WooCommerce users as well as simplify parts of the setup process. For example, we can do automated tax calculations for certain countries, significantly simplifying the tax task. To make this work, the user needs to be connected to a WordPress.com account. This also means development and testing of these features needs to be done on a WPCOM connected site. Search the MGS & the Field Guide for additional resources on testing WordPress.com with local setups.
We have a special Jetpack connection flow designed specifically for WooCommerce onboarding, so that the user feels that they are connecting as part of a cohesive experience. To access this flow, we have a custom Jetpack connection endpoint [/wc-admin/plugins/connect-jetpack](https://github.com/woocommerce/woocommerce/blob/feba6a8dcd55d4f5c7edc05478369c76df082293/plugins/woocommerce/src/Admin/API/Plugins.php#L395-L417). We have a special WordPress.com connection flow designed specifically for WooCommerce onboarding, so that the user feels that they are connecting as part of a cohesive experience. To access this flow, we use the [/wc-admin/onboarding/plugins/jetpack-authorization-url](https://github.com/woocommerce/woocommerce/blob/1a9c1f93b942f682b6561b5cd1ae58f6d5eea49c/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php#L240C2-L274) endpoint.
We use Jetpack's `build_connect_url` function directly, but add the following two query parameters: We use the Jetpack Connection package's `Manager::get_authorization_url()` function directly, but add the following two query parameters:
* `calypso_env`, which allows us to load different versions of Calypso when testing. See the Calypso section below. * `calypso_env`, which allows us to load different versions of Calypso when testing. See the Calypso section below.
* `from=woocommerce-onboarding`, which is used to conditionally show the WooCommerce themed Jetpack authorization process [https://github.com/Automattic/wp-calypso/pull/34380](https://github.com/Automattic/wp-calypso/pull/34380). Without this parameter, you would end up in the normal Jetpack authorization flow. * `from=woocommerce-services`, which is used to conditionally show the WooCommerce-themed authorization process [https://github.com/Automattic/wp-calypso/pull/35193](https://github.com/Automattic/wp-calypso/pull/35193). Without this parameter, you would end up in the normal Jetpack authorization flow.
The user is prompted to install and connect to Jetpack as the first step of the profile wizard. If the user hasn't connected when they arrive at the task list, we also prompt them on certain tasks to make the setup process easier, such as the shipping and tax steps. The user is prompted to install and connect to Jetpack as the first step of the profile wizard. If the user hasn't connected when they arrive at the task list, we also prompt them on certain tasks to make the setup process easier, such as the shipping and tax steps.
To disconnect from Jetpack, go to `Jetpack > Dashboard > Connections > Site connection > Manage site connection > Disconnect`. To disconnect from WordPress.com, install Jetpack, then go to `Jetpack > Dashboard > Connections > Site connection > Manage site connection > Disconnect`. You can remove Jetpack after you disconnect.
## Calypso ## Calypso

View File

@ -10,6 +10,8 @@ const BundleAnalyzerPlugin =
const MomentTimezoneDataPlugin = require( 'moment-timezone-data-webpack-plugin' ); const MomentTimezoneDataPlugin = require( 'moment-timezone-data-webpack-plugin' );
const ForkTsCheckerWebpackPlugin = require( 'fork-ts-checker-webpack-plugin' ); const ForkTsCheckerWebpackPlugin = require( 'fork-ts-checker-webpack-plugin' );
const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' ); const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' );
const NormalModuleReplacementPlugin =
require( 'webpack' ).NormalModuleReplacementPlugin;
/** /**
* Internal dependencies * Internal dependencies
@ -168,6 +170,14 @@ const webpackConfig = {
}, },
}, },
plugins: [ plugins: [
// Workaround for Gutenberg private API consent string differences between WP 6.3 and 6.4+
// The modified version checks for the WP version and replaces the consent string with the correct one.
// This can be removed once we drop support for WP 6.3 in the "Customize Your Store" task.
// See this PR for details: https://github.com/woocommerce/woocommerce/pull/40884
new NormalModuleReplacementPlugin(
/@wordpress\/edit-site\/build-module\/lock-unlock\.js/,
path.resolve( __dirname, 'bin/modified-editsite-lock-unlock.js' )
),
...styleConfig.plugins, ...styleConfig.plugins,
// Runs TypeScript type checker on a separate process. // Runs TypeScript type checker on a separate process.
! process.env.STORYBOOK && new ForkTsCheckerWebpackPlugin(), ! process.env.STORYBOOK && new ForkTsCheckerWebpackPlugin(),

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Do not remove sale date from when the sale is still active

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix detection of cart and checkout classic-shortcode blocks in the system status report.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix deprecation Passing null to parameter #1 ($datetime) of type string is deprecated

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Register product downloads block

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add virtual section and block to the Shipping tab

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add support for default values when generating variations in data store and REST API.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add attributes filter to variations endpoint and deprecate local_attributes filter.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add description to Variation options and Variations sections

View File

@ -1,4 +0,0 @@
Significance: patch
Type: add
Add product page skeleton to product and variation pages

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
This PR displays a warning modal when the `Design with A.I` button is clicked, but the `Customize Your Store` task has not been completed, and the active theme has modifications.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add support for digital product when product-virtual-downloadable feature is enabled

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add new product variation edit page and remove some unused product related components.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add new ProductVariationTemplate class and exposed it in the product editor settings, also enabled rest api for product variations.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Move product page footer from editor to product page, and update useIsScrolled hook.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Register image and visibility blocks into ProductVariationTemplate

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Update variation API to adhere tax class to context, and updated variation template to use tax class field.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Register the inventory section for product variation template

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Register the shipping section for product variation template

Some files were not shown because too many files have changed in this diff Show More