diff --git a/packages/js/product-editor/changelog/dev-40246_update_copy_in_variation_options_modal b/packages/js/product-editor/changelog/dev-40246_update_copy_in_variation_options_modal new file mode 100644 index 00000000000..b4b4f4edbdb --- /dev/null +++ b/packages/js/product-editor/changelog/dev-40246_update_copy_in_variation_options_modal @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update copy in the add variation options modal diff --git a/packages/js/product-editor/changelog/fix-new_product_editor_variation_actions_style b/packages/js/product-editor/changelog/fix-new_product_editor_variation_actions_style new file mode 100644 index 00000000000..9408bf3359a --- /dev/null +++ b/packages/js/product-editor/changelog/fix-new_product_editor_variation_actions_style @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix blocks product editor variation actions styles diff --git a/packages/js/product-editor/src/blocks/variation-options/edit.tsx b/packages/js/product-editor/src/blocks/variation-options/edit.tsx index d6f9d625f24..fd36230821b 100644 --- a/packages/js/product-editor/src/blocks/variation-options/edit.tsx +++ b/packages/js/product-editor/src/blocks/variation-options/edit.tsx @@ -130,20 +130,9 @@ export function Edit() { 'Add variation options', 'woocommerce' ), - newAttributeModalDescription: createInterpolateElement( - __( - 'Select from existing global attributes or create options for buyers to choose on the product page. You can change the order later.', - 'woocommerce' - ), - { - globalAttributeLink: ( - - ), - } + newAttributeModalDescription: __( + 'Select from existing attributes or create new ones to add new variations for your product. You can change the order later.', + 'woocommerce' ), attributeRemoveLabel: __( 'Remove variation option', diff --git a/packages/js/product-editor/src/blocks/variations/edit.tsx b/packages/js/product-editor/src/blocks/variations/edit.tsx index f49b88bc962..9b4af8e98cb 100644 --- a/packages/js/product-editor/src/blocks/variations/edit.tsx +++ b/packages/js/product-editor/src/blocks/variations/edit.tsx @@ -4,14 +4,9 @@ import classNames from 'classnames'; import type { BlockEditProps } from '@wordpress/blocks'; import { Button } from '@wordpress/components'; -import { Link } from '@woocommerce/components'; import { Product, ProductAttribute } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; -import { - createElement, - useState, - createInterpolateElement, -} from '@wordpress/element'; +import { createElement, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useBlockProps, @@ -131,20 +126,9 @@ export function Edit( { { isNewModalVisible && ( global attributes or create options for buyers to choose on the product page. You can change the order later.', - 'woocommerce' - ), - { - globalAttributeLink: ( - - ), - } + description={ __( + 'Select from existing attributes or create new ones to add new variations for your product. You can change the order later.', + 'woocommerce' ) } createNewAttributesAsGlobal={ true } notice={ '' } diff --git a/packages/js/product-editor/src/components/variations-table/styles.scss b/packages/js/product-editor/src/components/variations-table/styles.scss index 073d4f9a231..61c9c0ef026 100644 --- a/packages/js/product-editor/src/components/variations-table/styles.scss +++ b/packages/js/product-editor/src/components/variations-table/styles.scss @@ -41,6 +41,7 @@ $table-row-height: calc($grid-unit * 9); border-color: $gray-600; } } + margin-left: $gap-smallest; } &__filters { @@ -99,6 +100,7 @@ $table-row-height: calc($grid-unit * 9); align-items: center; justify-content: flex-end; gap: $gap-smaller; + margin-right: $gap-smallest; &--delete { &.components-button.components-menu-item__button.is-link { diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/resizable-frame.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/resizable-frame.jsx index 0af6db93f08..ef9d8e66c64 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/resizable-frame.jsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/resizable-frame.jsx @@ -180,13 +180,12 @@ function ResizableFrame( { left: 0, }, visible: { - opacity: 1, + opacity: 0.6, left: -10, }, active: { opacity: 1, left: -10, - scaleY: 1.3, }, }; const currentResizeHandleVariant = ( () => { diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx index c6107b9d25c..c9199f3a039 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx @@ -4,15 +4,19 @@ */ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; +import { useContext } from '@wordpress/element'; +import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider'; const { useGlobalStyle, useGlobalSetting, useSettingsForBlockElement, ColorPanel: StylesColorPanel, + GlobalStylesContext, } = unlock( blockEditorPrivateApis ); export const ColorPanel = () => { + const { setUserConfig } = useContext( GlobalStylesContext ); const [ style ] = useGlobalStyle( '', undefined, 'user', { shouldDecodeEncode: false, } ); @@ -22,11 +26,25 @@ export const ColorPanel = () => { const [ rawSettings ] = useGlobalSetting( '' ); const settings = useSettingsForBlockElement( rawSettings ); + const onChange = ( ...props ) => { + setStyle( ...props ); + setUserConfig( ( currentConfig ) => ( { + ...currentConfig, + settings: mergeBaseAndUserConfigs( currentConfig.settings, { + color: { + palette: { + hasCreatedOwnColors: true, + }, + }, + } ), + } ) ); + }; + return ( ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx index 71aabc0679a..50a751b84d1 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx @@ -28,6 +28,15 @@ export const VariationContainer = ( { variation, children } ) => { }, [ variation, base ] ); const selectVariation = () => { + // Remove the hasCreatedOwnColors flag if the user is switching to a color palette + if ( + variation.settings.color && + user.settings.color && + user.settings.color.hasCreatedOwnColors + ) { + delete user.settings.color.palette.hasCreatedOwnColors; + } + setUserConfig( () => { return { settings: mergeBaseAndUserConfigs( @@ -48,7 +57,6 @@ export const VariationContainer = ( { variation, children } ) => { selectVariation(); } }; - const isActive = useMemo( () => { if ( variation.settings.color ) { return isEqual( variation.settings.color, user.settings.color ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx index 38e214133dc..c862af81453 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx @@ -147,8 +147,8 @@ function Sidebar() { initialPath={ initialPath.current } > + - ); } diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx index 882b1df989e..d28064da0cf 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx @@ -10,6 +10,8 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { // @ts-ignore No types for this exist yet. __experimentalHStack as HStack, + // @ts-ignore No types for this exist yet. + __experimentalUseNavigator as useNavigator, Button, Spinner, } from '@wordpress/components'; @@ -41,13 +43,13 @@ export const SaveHub = () => { const urlParams = useQuery(); const { sendEvent } = useContext( CustomizeStoreContext ); const [ isResolving, setIsResolving ] = useState< boolean >( false ); + const navigator = useNavigator(); // @ts-ignore No types for this exist yet. const { __unstableMarkLastChangeAsPersistent } = useDispatch( blockEditorStore ); - const { createSuccessNotice, createErrorNotice, removeNotice } = - useDispatch( noticesStore ); + const { createErrorNotice, removeNotice } = useDispatch( noticesStore ); const { dirtyEntityRecords, @@ -176,10 +178,7 @@ export const SaveHub = () => { } } - createSuccessNotice( __( 'Site updated.', 'woocommerce' ), { - type: 'snackbar', - id: saveNoticeId, - } ); + navigator.goToParent(); } catch ( error ) { createErrorNotice( `${ __( 'Saving failed.', 'woocommerce' ) } ${ error }` diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss index ae92f2895ea..31061aa4d33 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss @@ -119,10 +119,14 @@ .edit-site-layout__sidebar { .edit-site-sidebar__content { + display: flex; + flex-direction: column; + .components-navigator-screen { will-change: auto; padding: 0 16px; overflow-x: hidden; + flex: 1; } } @@ -431,7 +435,8 @@ .edit-site-layout__canvas { bottom: 16px; top: 16px; - padding: 0 16px; + left: 12px; // the default styles for this undersizes the width by 24px so we want to center this + padding: 0 4px 0 16px; } .edit-site-resizable-frame__handle { @@ -442,13 +447,13 @@ padding: 0 10px; align-items: flex-start; gap: 10px; - background: #d7defb; + background: var(--wp-admin-theme-color-background-25); left: 5px !important; border-radius: 4px; height: 20px; .components-popover__content { - color: #1d35b4; + color: var(--wp-admin-theme-color-darker-20); font-size: 0.75rem; font-style: normal; font-weight: 500; @@ -549,42 +554,3 @@ margin-left: 12px; } } - -.woocommerce-customize-store_global-styles-variations_item { - border-radius: 2px; - padding: 2.5px; - - .woocommerce-customize-store_global-styles-variations_item-preview { - border: 1px solid #dcdcde; - background: #fff; - } - - &:hover, - &.is-active { - box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color), 0 0 0 2.5px #fff; - } -} - -.edit-site-sidebar-navigation-screen-patterns__group-homepage { - .woocommerce-collapsible-content:last-child { - border-bottom: none; - } - .woocommerce-collapsible-content { - padding: 16px 0 16px 0; - border-bottom: 1px solid #ededed; - button { - width: 100%; - color: #1e1e1e; - font-size: 14px; - font-weight: 500; - line-height: 20px; - letter-spacing: -0.15px; - } - svg { - margin-left: auto; - fill: #1e1e1e; - width: 20px; - height: 20px; - } - } -} diff --git a/plugins/woocommerce-admin/client/customize-store/assets/images/banner-design-with-ai.svg b/plugins/woocommerce-admin/client/customize-store/assets/images/banner-design-with-ai.svg new file mode 100644 index 00000000000..3fc2ac8a365 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assets/images/banner-design-with-ai.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/actions.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/actions.ts index 661504275c1..f51259c427a 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/actions.ts +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/actions.ts @@ -9,9 +9,10 @@ import { recordEvent } from '@woocommerce/tracks'; * Internal dependencies */ import { - ColorPalette, + ColorPaletteResponse, designWithAiStateMachineContext, designWithAiStateMachineEvents, + FontPairing, LookAndToneCompletionResponse, } from './types'; import { aiWizardClosedBeforeCompletionEvent } from './events'; @@ -83,7 +84,7 @@ const assignDefaultColorPalette = assign< defaultColorPalette: ( event as { data: { - response: ColorPalette; + response: ColorPaletteResponse; }; } ).data.response, @@ -91,6 +92,24 @@ const assignDefaultColorPalette = assign< }, } ); +const assignFontPairing = assign< + designWithAiStateMachineContext, + designWithAiStateMachineEvents +>( { + aiSuggestions: ( context, event: unknown ) => { + return { + ...context.aiSuggestions, + fontPairing: ( + event as { + data: { + response: FontPairing; + }; + } + ).data.response.pair_name, + }; + }, +} ); + const logAIAPIRequestError = () => { // log AI API request error // eslint-disable-next-line no-console @@ -158,6 +177,7 @@ export const actions = { assignToneOfVoice, assignLookAndTone, assignDefaultColorPalette, + assignFontPairing, logAIAPIRequestError, updateQueryStep, recordTracksStepViewed, diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/components/choice/choice.scss b/plugins/woocommerce-admin/client/customize-store/design-with-ai/components/choice/choice.scss index e90b762ba9d..71788874562 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/components/choice/choice.scss +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/components/choice/choice.scss @@ -35,6 +35,7 @@ font-weight: 500; line-height: 20px; /* 125% */ letter-spacing: -0.24px; + margin-bottom: 8px; } p { diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/LookAndFeel.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/LookAndFeel.tsx index ec77bf542cf..f6f748d8f39 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/LookAndFeel.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/LookAndFeel.tsx @@ -33,7 +33,7 @@ export const LookAndFeel = ( { title: __( 'Contemporary', 'woocommerce' ), key: 'Contemporary' as const, subtitle: __( - 'Clean lines, neutral colors, sleek and modern look', + 'Clean lines, neutral colors, sleek and modern look.', 'woocommerce' ), }, diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ToneOfVoice.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ToneOfVoice.tsx index b832a0d29cc..16ef43026f3 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ToneOfVoice.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ToneOfVoice.tsx @@ -77,7 +77,10 @@ export const ToneOfVoice = ( {

- { __( 'How would you like to sound?', 'woocommerce' ) } + { __( + 'Which writing style do you prefer?', + 'woocommerce' + ) }

{ choices.map( ( { title, subtitle, key } ) => { diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/colorChoices.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/colorChoices.ts index 5fa682b0aee..2eaba5f279b 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/colorChoices.ts +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/colorChoices.ts @@ -200,11 +200,14 @@ const colorChoices: ColorPalette[] = [ ]; const allowedNames: string[] = colorChoices.map( ( palette ) => palette.name ); const hexColorRegex = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/; +const colorPaletteNameValidator = z + .string() + .refine( ( name ) => allowedNames.includes( name ), { + message: 'Color palette not part of allowed list', + } ); export const colorPaletteValidator = z.object( { - name: z.string().refine( ( name ) => allowedNames.includes( name ), { - message: 'Color palette not part of allowed list', - } ), + name: colorPaletteNameValidator, primary: z .string() .regex( hexColorRegex, { message: 'Invalid primary color' } ), @@ -219,6 +222,11 @@ export const colorPaletteValidator = z.object( { .regex( hexColorRegex, { message: 'Invalid background color' } ), } ); +export const colorPaletteResponseValidator = z.object( { + default: colorPaletteNameValidator, + bestColors: z.array( colorPaletteNameValidator ).length( 8 ), +} ); + export const defaultColorPalette = { queryId: 'default_color_palette', @@ -226,8 +234,8 @@ export const defaultColorPalette = { version: '2023-09-18', prompt: ( businessDescription: string, look: string, tone: string ) => { return ` - You are a WordPress theme expert. Analyse the following store description, merchant's chosen look and tone, and determine the most appropriate color scheme. - Respond only with one color scheme and only its JSON. + You are a WordPress theme expert. Analyse the following store description, merchant's chosen look and tone, and determine the most appropriate color scheme, along with 8 best alternatives. + Respond in the form: "{ default: "palette name", bestColors: [ "palette name 1", "palette name 2", "palette name 3", "palette name 4", "palette name 5", "palette name 6", "palette name 7", "palette name 8" ] }" Chosen look and tone: ${ look } look, ${ tone } tone. Business description: ${ businessDescription } @@ -236,5 +244,5 @@ export const defaultColorPalette = { ${ JSON.stringify( colorChoices ) } `; }, - responseValidation: colorPaletteValidator.parse, + responseValidation: colorPaletteResponseValidator.parse, }; diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/fontPairings.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/fontPairings.ts new file mode 100644 index 00000000000..e8af1cb7549 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/fontPairings.ts @@ -0,0 +1,120 @@ +/** + * External dependencies + */ +import { z } from 'zod'; + +/** This block below was generated by ChatGPT using GPT-4 on 2023-09-18 */ +/** Original source: plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts */ +const fontChoices = [ + { + pair_name: 'Bodoni Moda + Overpass', + fonts: { + 'Bodoni Moda': + 'A modern serif font with high contrast between thick and thin lines, commonly used for headings.', + Overpass: + 'A clean, modern sans-serif, originally inspired by Highway Gothic. Good for text and UI elements.', + }, + settings: + 'Overpass is used for buttons and general typography, while Bodoni Moda is specified for headings and some core blocks like site title and post navigation link.', + }, + { + pair_name: 'Commissioner + Crimson Pro', + fonts: { + Commissioner: + 'A low-contrast, geometric sans-serif, designed for legibility and readability in long texts.', + 'Crimson Pro': + 'A serif typeface designed for readability and long-form text.', + }, + settings: + 'Commissioner dominates elements like buttons, headings, and core blocks, while Crimson Pro is set for general typography.', + }, + { + pair_name: 'Libre Baskerville + DM Sans', + fonts: { + 'Libre Baskerville': + 'A serif typeface with a classic feel, good for long reading and often used for body text in books.', + 'DM Sans': + 'A clean, geometric sans-serif, often used for UI and short text.', + }, + settings: + 'Libre Baskerville is used for headings and core blocks, whereas DM Sans is used for buttons and general typography.', + }, + { + pair_name: 'Libre Franklin + EB Garamond', + fonts: { + 'Libre Franklin': + 'A sans-serif that offers readability, suitable for both text and display.', + 'EB Garamond': + "A revival of the classical 'Garamond' typefaces, suitable for long-form text.", + }, + settings: + 'Libre Franklin is predominantly used for elements like buttons, headings, and core blocks. EB Garamond is set for general typography.', + }, + { + pair_name: 'Montserrat + Arvo', + fonts: { + Montserrat: + 'A geometric sans-serif, popular for its modern clean lines.', + Arvo: 'A slab-serif font with a more traditional feel, suitable for print and screen.', + }, + settings: + 'Montserrat is used for buttons, headings, and core blocks. Arvo is used for general typography.', + }, + { + pair_name: 'Playfair Display + Fira Sans', + fonts: { + 'Playfair Display': + 'A high-contrast serif designed for headings and offers a modern take on older serif fonts.', + 'Fira Sans': + 'A sans-serif designed for readability at small sizes, making it suitable for both UI and text.', + }, + settings: + 'Playfair Display is used in italics for headings and core blocks, while Fira Sans is used for buttons and general typography.', + }, + { + pair_name: 'Rubik + Inter', + fonts: { + Rubik: 'A sans-serif with slightly rounded corners, designed for a softer, more modern look.', + Inter: 'A highly legible sans-serif, optimized for UI design.', + }, + settings: + 'Rubik is applied for headings and core blocks. Inter is used for buttons and general typography.', + }, + { + pair_name: 'Space Mono + Roboto', + fonts: { + 'Space Mono': 'A monospace typeface with a futuristic vibe.', + Roboto: 'A neo-grotesque sans-serif, known for its flexibility and modern design.', + }, + settings: + 'Space Mono is used for headings, while Roboto takes care of buttons and general typography.', + }, +]; + +const allowedFontChoices = fontChoices.map( ( config ) => config.pair_name ); + +export const fontChoiceValidator = z.object( { + pair_name: z + .string() + .refine( ( name ) => allowedFontChoices.includes( name ), { + message: 'Font choice not part of allowed list', + } ), +} ); + +export const fontPairings = { + queryId: 'font_pairings', + version: '2023-09-18', + prompt: ( businessDescription: string, look: string, tone: string ) => { + return ` + You are a WordPress theme expert. Analyse the following store description, merchant's chosen look and tone, and determine the most appropriate font pairing. + Respond only with one font pairing and and in the format: '{"pair_name":"font 1 + font 2"}'. + + Chosen look and tone: ${ look } look, ${ tone } tone. + Business description: ${ businessDescription } + + Font pairings to choose from: + ${ JSON.stringify( fontChoices ) } + `; + }, + responseValidation: fontChoiceValidator.parse, +}; diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/index.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/index.ts index ee134ed5941..b0533ec1dc8 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/index.ts +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/index.ts @@ -1,2 +1,3 @@ export * from './colorChoices'; export * from './lookAndTone'; +export * from './fontPairings'; diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/colorChoices.test.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/colorChoices.test.ts similarity index 55% rename from plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/colorChoices.test.ts rename to plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/colorChoices.test.ts index 91c7d0df06e..d1aae496b45 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/colorChoices.test.ts +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/colorChoices.test.ts @@ -1,9 +1,9 @@ /** * Internal dependencies */ -import { defaultColorPalette } from '.'; +import { colorPaletteValidator, defaultColorPalette } from '..'; -describe( 'colorPairing.responseValidation', () => { +describe( 'colorPaletteValidator', () => { it( 'should validate a correct color palette', () => { const validPalette = { name: 'Ancient Bronze', @@ -13,8 +13,7 @@ describe( 'colorPairing.responseValidation', () => { background: '#ffffff', }; - const parsedResult = - defaultColorPalette.responseValidation( validPalette ); + const parsedResult = colorPaletteValidator.parse( validPalette ); expect( parsedResult ).toEqual( validPalette ); } ); @@ -26,7 +25,7 @@ describe( 'colorPairing.responseValidation', () => { foreground: '#11163d', background: '#ffffff', }; - expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + expect( () => colorPaletteValidator.parse( invalidPalette ) ) .toThrowErrorMatchingInlineSnapshot( ` "[ { @@ -48,7 +47,7 @@ describe( 'colorPairing.responseValidation', () => { foreground: '#11163d', background: '#ffffff', }; - expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + expect( () => colorPaletteValidator.parse( invalidPalette ) ) .toThrowErrorMatchingInlineSnapshot( ` "[ { @@ -71,7 +70,7 @@ describe( 'colorPairing.responseValidation', () => { foreground: '#11163d', background: '#ffffff', }; - expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + expect( () => colorPaletteValidator.parse( invalidPalette ) ) .toThrowErrorMatchingInlineSnapshot( ` "[ { @@ -94,7 +93,7 @@ describe( 'colorPairing.responseValidation', () => { foreground: '#invalid_color', background: '#ffffff', }; - expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + expect( () => colorPaletteValidator.parse( invalidPalette ) ) .toThrowErrorMatchingInlineSnapshot( ` "[ { @@ -125,7 +124,7 @@ describe( 'colorPairing.responseValidation', () => { foreground: '#11163d', background: '#fffff', }; - expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + expect( () => colorPaletteValidator.parse( invalidPalette ) ) .toThrowErrorMatchingInlineSnapshot( ` "[ { @@ -140,3 +139,100 @@ describe( 'colorPairing.responseValidation', () => { ` ); } ); } ); + +describe( 'colorPaletteResponseValidator', () => { + it( 'should validate a correct color palette response', () => { + const validPalette = { + default: 'Ancient Bronze', + bestColors: Array( 8 ).fill( 'Ancient Bronze' ), + }; + + const parsedResult = + defaultColorPalette.responseValidation( validPalette ); + expect( parsedResult ).toEqual( validPalette ); + } ); + + it( 'should fail if array contains invalid color', () => { + const invalidPalette = { + default: 'Ancient Bronze', + bestColors: Array( 7 ) + .fill( 'Ancient Bronze' ) + .concat( [ 'Invalid Color' ] ), + }; + expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + .toThrowErrorMatchingInlineSnapshot( ` + "[ + { + \\"code\\": \\"custom\\", + \\"message\\": \\"Color palette not part of allowed list\\", + \\"path\\": [ + \\"bestColors\\", + 7 + ] + } + ]" + ` ); + } ); + + it( 'should fail if bestColors property is missing', () => { + const invalidPalette = { + default: 'Ancient Bronze', + }; + expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + .toThrowErrorMatchingInlineSnapshot( ` + "[ + { + \\"code\\": \\"invalid_type\\", + \\"expected\\": \\"array\\", + \\"received\\": \\"undefined\\", + \\"path\\": [ + \\"bestColors\\" + ], + \\"message\\": \\"Required\\" + } + ]" + ` ); + } ); + it( 'should fail if default property is missing', () => { + const invalidPalette = { + bestColors: Array( 8 ).fill( 'Ancient Bronze' ), + }; + expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + .toThrowErrorMatchingInlineSnapshot( ` + "[ + { + \\"code\\": \\"invalid_type\\", + \\"expected\\": \\"string\\", + \\"received\\": \\"undefined\\", + \\"path\\": [ + \\"default\\" + ], + \\"message\\": \\"Required\\" + } + ]" + ` ); + } ); + + it( 'should fail if bestColors array is not of length 8', () => { + const invalidPalette = { + default: 'Ancient Bronze', + bestColors: Array( 7 ).fill( 'Ancient Bronze' ), + }; + expect( () => defaultColorPalette.responseValidation( invalidPalette ) ) + .toThrowErrorMatchingInlineSnapshot( ` + "[ + { + \\"code\\": \\"too_small\\", + \\"minimum\\": 8, + \\"type\\": \\"array\\", + \\"inclusive\\": true, + \\"exact\\": true, + \\"message\\": \\"Array must contain exactly 8 element(s)\\", + \\"path\\": [ + \\"bestColors\\" + ] + } + ]" + ` ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/fontPairings.test.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/fontPairings.test.ts new file mode 100644 index 00000000000..dfb132a8e24 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/fontPairings.test.ts @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import { fontChoiceValidator } from '..'; + +describe( 'fontChoiceValidator', () => { + it( 'should validate when font choice is part of the allowed list', () => { + const validFontChoice = { pair_name: 'Montserrat + Arvo' }; + expect( () => + fontChoiceValidator.parse( validFontChoice ) + ).not.toThrow(); + } ); + + it( 'should not validate when font choice is not part of the allowed list', () => { + const invalidFontChoice = { pair_name: 'Comic Sans' }; + expect( () => fontChoiceValidator.parse( invalidFontChoice ) ) + .toThrowErrorMatchingInlineSnapshot( ` + "[ + { + \\"code\\": \\"custom\\", + \\"message\\": \\"Font choice not part of allowed list\\", + \\"path\\": [ + \\"pair_name\\" + ] + } + ]" + ` ); + } ); + + it( 'should not validate when pair_name is not a string', () => { + const invalidType = { pair_name: 123 }; + expect( () => fontChoiceValidator.parse( invalidType ) ) + .toThrowErrorMatchingInlineSnapshot( ` + "[ + { + \\"code\\": \\"invalid_type\\", + \\"expected\\": \\"string\\", + \\"received\\": \\"number\\", + \\"path\\": [ + \\"pair_name\\" + ], + \\"message\\": \\"Expected string, received number\\" + } + ]" + ` ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/lookAndTone.test.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/lookAndTone.test.ts similarity index 89% rename from plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/lookAndTone.test.ts rename to plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/lookAndTone.test.ts index cea15cc91b2..a5957d25a3e 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/lookAndTone.test.ts +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/prompts/test/lookAndTone.test.ts @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import { LookAndToneCompletionResponse } from '../types'; -import { lookAndTone } from '.'; +import { LookAndToneCompletionResponse } from '../../types'; +import { lookAndTone } from '..'; describe( 'parseLookAndToneCompletionResponse', () => { it( 'should return a valid object when given valid JSON', () => { diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/state-machine.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/state-machine.tsx index 58dd0367583..50bc7fb8298 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/state-machine.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/state-machine.tsx @@ -10,7 +10,8 @@ import { getQuery } from '@woocommerce/navigation'; import { designWithAiStateMachineContext, designWithAiStateMachineEvents, - ColorPalette, + FontPairing, + ColorPaletteResponse, } from './types'; import { BusinessInfoDescription, @@ -20,7 +21,7 @@ import { } from './pages'; import { actions } from './actions'; import { services } from './services'; -import { defaultColorPalette } from './prompts'; +import { defaultColorPalette, fontPairings } from './prompts'; export const hasStepInUrl = ( _ctx: unknown, @@ -69,7 +70,8 @@ export const designWithAiStateMachineDefinition = createMachine( choice: '', }, aiSuggestions: { - defaultColorPalette: {} as ColorPalette, + defaultColorPalette: {} as ColorPaletteResponse, + fontPairing: '' as FontPairing[ 'pair_name' ], }, }, initial: 'navigate', @@ -291,6 +293,25 @@ export const designWithAiStateMachineDefinition = createMachine( }, }, }, + chooseFontPairing: { + invoke: { + src: 'queryAiEndpoint', + data: ( context ) => { + return { + ...fontPairings, + prompt: fontPairings.prompt( + context.businessInfoDescription + .descriptionText, + context.lookAndFeel.choice, + context.toneOfVoice.choice + ), + }; + }, + onDone: { + actions: [ 'assignFontPairing' ], + }, + }, + }, }, }, postApiCallLoader: {}, diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/types.ts b/plugins/woocommerce-admin/client/customize-store/design-with-ai/types.ts index 743d094d1c4..a639755078e 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/types.ts +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/types.ts @@ -5,7 +5,11 @@ import { z } from 'zod'; /** * Internal dependencies */ -import { colorPaletteValidator } from './prompts'; +import { + colorPaletteValidator, + colorPaletteResponseValidator, + fontChoiceValidator, +} from './prompts'; export type designWithAiStateMachineContext = { businessInfoDescription: { @@ -18,7 +22,8 @@ export type designWithAiStateMachineContext = { choice: Tone | ''; }; aiSuggestions: { - defaultColorPalette: ColorPalette; + defaultColorPalette: ColorPaletteResponse; + fontPairing: FontPairing[ 'pair_name' ]; }; // If we require more data from options, previously provided core profiler details, // we can retrieve them in preBusinessInfoDescription and then assign them here @@ -50,3 +55,8 @@ export interface LookAndToneCompletionResponse { } export type ColorPalette = z.infer< typeof colorPaletteValidator >; +export type ColorPaletteResponse = z.infer< + typeof colorPaletteResponseValidator +>; + +export type FontPairing = z.infer< typeof fontChoiceValidator >; diff --git a/plugins/woocommerce-admin/client/customize-store/intro/index.tsx b/plugins/woocommerce-admin/client/customize-store/intro/index.tsx index 9e06b0e8c77..c498a3667bb 100644 --- a/plugins/woocommerce-admin/client/customize-store/intro/index.tsx +++ b/plugins/woocommerce-admin/client/customize-store/intro/index.tsx @@ -1,8 +1,17 @@ +/** + * External dependencies + */ + +import { __ } from '@wordpress/i18n'; +import { chevronLeft } from '@wordpress/icons'; + /** * Internal dependencies */ import { CustomizeStoreComponent } from '../types'; +import './intro.scss'; + export type events = | { type: 'DESIGN_WITH_AI' } | { type: 'CLICKED_ON_BREADCRUMB' } @@ -15,33 +24,106 @@ export * as services from './services'; export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => { const { - intro: { themeCards, activeTheme }, + intro: { themeCards }, } = context; + return ( <> -

Intro

-
Active theme: { activeTheme }
- { themeCards?.map( ( themeCard ) => ( - - ) ) } - - +
+

{ 'Site title' }

+
+ +
+
+
+ + { __( 'Customize your store', 'woocommerce' ) } +
+

+ { __( + 'Create a store that reflects your brand and business. Select one of our professionally designed themes to customize, or create your own using AI.', + 'woocommerce' + ) } +

+
+ +
+
+
+

+ { __( + 'Use the power of AI to design your store', + 'woocommerce' + ) } +

+

+ { __( + 'Design the look of your store, create pages, and generate copy using our built-in AI tools.', + 'woocommerce' + ) } +

+ +
+
+ +

+ { __( + 'Or select a professionally designed theme to customize and make your own.', + 'woocommerce' + ) } +

+ +
+ { themeCards?.map( ( themeCard ) => ( +
+
+ { +
+

+ { themeCard.name } +

+
+ ) ) } +
+ +
+ +
+ + +
+
); }; diff --git a/plugins/woocommerce-admin/client/customize-store/intro/intro.scss b/plugins/woocommerce-admin/client/customize-store/intro/intro.scss new file mode 100644 index 00000000000..c579d9295a0 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/intro/intro.scss @@ -0,0 +1,155 @@ +.woocommerce-profile-wizard__container { + display: flex; + flex-direction: column; + + button { + cursor: pointer; + } +} + +.woocommerce-customize-store-header { + min-height: 64px; + padding: 1rem; + width: 100%; + + h1 { + font-size: 0.8125rem; + font-weight: 500; + margin: 0; + padding: 0; + line-height: 1.5rem; + } + +} + +.woocommerce-customize-store-container { + display: flex; + flex-direction: row; +} + +.woocommerce-customize-store-sidebar { + flex: 0 0 380px; + padding: 1rem; + + .woocommerce-customize-store-sidebar__title { + color: #1e1e1e; + font-size: 1rem; + font-weight: 600; + margin: 0; + padding: 0; + line-height: 2.5; + } + + button { + background-color: transparent; + border: none; + line-height: 1; + padding-right: 0; + vertical-align: middle; + } + + svg { + color: inherit; + height: 24px; + width: 24px; + margin: 0.25rem; + } + + p { + padding: 0 1rem; + color: #757575; + max-width: 20rem; + } +} + +.woocommerce-customize-store-main { + margin-right: 2.5rem; + + p { + color: #2f2f2f; + font-size: 1rem; + line-height: 1.5; + margin-bottom: 1.5rem; + } +} + +.woocommerce-customize-store-banner { + background: var(--woo-purple-woo-purple-0, #f2edff); + background-image: url(../assets/images/banner-design-with-ai.svg); + background-repeat: no-repeat; + background-position: bottom right; + background-size: contain; + border-radius: 4px; + display: flex; + margin: 1.25rem 0 3.375rem; + min-height: 343px; + padding: 70px 0; + width: 820px; + + .woocommerce-customize-store-banner-content { + width: 345px; + margin-left: 50px; + + h1 { + font-size: 1.5rem; + line-height: 1.33; + } + + p { + margin: 1rem 0 2rem 0; + } + + button { + background-color: #3858e9; + border: none; + border-radius: 2px; + color: #fff; + display: inline-block; + line-height: 1.25rem; + padding: 10px 15px; + + &:hover { + background-color: #2234e0; + } + } + + } +} + +.woocommerce-customize-store-theme-cards { + display: flex; + flex-wrap: wrap; + gap: 2rem; + max-width: 820px; + + .theme-card { + flex-basis: 45%; + + img { + border-radius: 4px; + border: 1px solid #e9e9e9; + width: 394px; + } + + .theme-card__title { + font-size: 1rem; + font-weight: 600; + margin: 1.5rem 0 0.5rem; + } + } +} + +.woocommerce-customize-store-browse-themes { + text-align: center; + + button { + background-color: #fff; + border: 1px solid #3858e9; + border-radius: 2px; + color: #3858e9; + display: inline-block; + font-size: 0.8125rem; + margin: 3.75rem 0; + padding: 0.5rem 0.75rem; + } +} diff --git a/plugins/woocommerce-admin/client/customize-store/intro/services.ts b/plugins/woocommerce-admin/client/customize-store/intro/services.ts index 37c282ab5a4..5fa170f3e9f 100644 --- a/plugins/woocommerce-admin/client/customize-store/intro/services.ts +++ b/plugins/woocommerce-admin/client/customize-store/intro/services.ts @@ -3,12 +3,55 @@ export const fetchThemeCards = async () => { return [ { + slug: 'twentytwentyone', name: 'Twenty Twenty One', description: 'The default theme for WordPress.', + image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/pub/twentytwentyone/screenshot.png', + styleVariations: [], }, { + slug: 'twentytwenty', name: 'Twenty Twenty', description: 'The previous default theme for WordPress.', + image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/pub/twentytwenty/screenshot.png', + styleVariations: [], + }, + { + slug: 'tsubaki', + name: 'Tsubaki', + description: + 'Tsubaki puts the spotlight on your products and your customers. This theme leverages WooCommerce to provide you with intuitive product navigation and the patterns you need to master digital merchandising.', + image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/premium/tsubaki/screenshot.png', + styleVariations: [], + }, + { + slug: 'winkel', + name: 'Winkel', + description: + 'Winkel is a minimal, product-focused theme featuring Payments block. Its clean, cool look combined with a simple layout makes it perfect for showcasing fashion items – clothes, shoes, and accessories.', + image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/pub/winkel/screenshot.png', + styleVariations: [ + { + title: 'Default', + primary: '#ffffff', + secondary: '#676767', + }, + { + title: 'Charcoal', + primary: '#1f2527', + secondary: '#9fd3e8', + }, + { + title: 'Rainforest', + primary: '#eef4f7', + secondary: '#35845d', + }, + { + title: 'Ruby Wine', + primary: '#ffffff', + secondary: '#c8133e', + }, + ], }, ]; }; diff --git a/plugins/woocommerce-admin/client/customize-store/intro/theme-cards.tsx b/plugins/woocommerce-admin/client/customize-store/intro/theme-cards.tsx index a67b23534ae..7c8ca0f7ea3 100644 --- a/plugins/woocommerce-admin/client/customize-store/intro/theme-cards.tsx +++ b/plugins/woocommerce-admin/client/customize-store/intro/theme-cards.tsx @@ -1,6 +1,9 @@ export type ThemeCard = { // placeholder props, possibly take reference from https://github.com/Automattic/wp-calypso/blob/1f1b79210c49ef0d051f8966e24122229a334e29/packages/design-picker/src/components/theme-card/index.tsx#L32 + slug: string; name: string; description: string; image: string; + isActive: boolean; + styleVariations: string[]; }; diff --git a/plugins/woocommerce-admin/client/customize-store/style.scss b/plugins/woocommerce-admin/client/customize-store/style.scss index b6fb0524d48..66e6c91a481 100644 --- a/plugins/woocommerce-admin/client/customize-store/style.scss +++ b/plugins/woocommerce-admin/client/customize-store/style.scss @@ -20,6 +20,10 @@ body.woocommerce-customize-store.js.is-fullscreen-mode { margin-top: 0 !important; height: 100%; + + & > div.tour-kit.woocommerce-tour-kit > div > div.tour-kit-spotlight.is-visible { + outline: 99999px solid rgba(0, 0, 0, 0.15); + } } .woocommerce-cys-layout { @@ -78,7 +82,7 @@ body.woocommerce-customize-store.js.is-fullscreen-mode { padding: 12px; width: 404px; border-radius: 2px; - background: var(--gutenberg-transparent-blueberry, rgba(56, 88, 233, 0.04)); + background: var(--wp-admin-theme-color-background-04, rgba(168, 168, 170, 0.301)); color: var(--gutenberg-gray-800, #2f2f2f); p { diff --git a/plugins/woocommerce-admin/client/index.js b/plugins/woocommerce-admin/client/index.js index 9feefdbba9e..07a2190c324 100644 --- a/plugins/woocommerce-admin/client/index.js +++ b/plugins/woocommerce-admin/client/index.js @@ -19,6 +19,7 @@ import { EmbeddedBodyLayout } from './embedded-body-layout'; import { WcAdminPaymentsGatewaysBannerSlot } from './payments/payments-settings-banner-slotfill'; import { WcAdminConflictErrorSlot } from './settings/conflict-error-slotfill.js'; import './xstate.js'; +import { deriveWpAdminBackgroundColours } from './utils/derive-wp-admin-background-colours'; // Modify webpack pubilcPath at runtime based on location of WordPress Plugin. // eslint-disable-next-line no-undef,camelcase @@ -48,6 +49,8 @@ const embeddedRoot = document.getElementById( 'woocommerce-embedded-root' ); const settingsGroup = 'wc_admin'; const hydrateUser = getAdminSetting( 'currentUserData' ); +deriveWpAdminBackgroundColours(); + if ( appRoot ) { let HydratedPageLayout = withSettingsHydration( settingsGroup, diff --git a/plugins/woocommerce-admin/client/task-lists/fills/appearance.js b/plugins/woocommerce-admin/client/task-lists/fills/appearance.js index bdc9a0f48d2..095f1c9b9b3 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/appearance.js +++ b/plugins/woocommerce-admin/client/task-lists/fills/appearance.js @@ -1,431 +1,36 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; -import { Button, Card, CardBody } from '@wordpress/components'; -import { Component, Fragment } from '@wordpress/element'; -import { compose } from '@wordpress/compose'; -import { filter } from 'lodash'; -import { withDispatch, withSelect } from '@wordpress/data'; - -import { Stepper, TextControl, ImageUpload } from '@woocommerce/components'; -import { - OPTIONS_STORE_NAME, - ONBOARDING_STORE_NAME, - WC_ADMIN_NAMESPACE, -} from '@woocommerce/data'; -import { queueRecordEvent, recordEvent } from '@woocommerce/tracks'; +import React from 'react'; +import { WooOnboardingTaskListItem } from '@woocommerce/onboarding'; import { registerPlugin } from '@wordpress/plugins'; -import { WooOnboardingTask } from '@woocommerce/onboarding'; +import { getAdminLink } from '@woocommerce/settings'; -/** - * Internal dependencies - */ -class Appearance extends Component { - constructor( props ) { - super( props ); - const { hasHomepage, hasProducts, supportCustomLogo } = - props.task.additionalData; - - this.stepVisibility = { - homepage: ! hasHomepage, - import: ! hasProducts, - logo: supportCustomLogo, - }; - - this.state = { - isDirty: false, - isPending: false, - logo: null, - stepIndex: 0, - isUpdatingLogo: false, - isUpdatingNotice: false, - storeNoticeText: props.demoStoreNotice || '', - }; - - this.completeStep = this.completeStep.bind( this ); - this.createHomepage = this.createHomepage.bind( this ); - this.importProducts = this.importProducts.bind( this ); - this.updateLogo = this.updateLogo.bind( this ); - this.updateNotice = this.updateNotice.bind( this ); - } - - componentDidMount() { - const { themeMods } = this.props.task.additionalData; - - if ( themeMods && themeMods.custom_logo ) { - /* eslint-disable react/no-did-mount-set-state */ - this.setState( { logo: { id: themeMods.custom_logo } } ); - /* eslint-enable react/no-did-mount-set-state */ - } - } - - componentDidUpdate( prevProps ) { - const { isPending, logo } = this.state; - const { demoStoreNotice } = this.props; - - if ( logo && ! logo.url && ! isPending ) { - /* eslint-disable react/no-did-update-set-state */ - this.setState( { isPending: true } ); - wp.media - .attachment( logo.id ) - .fetch() - .then( () => { - const logoUrl = wp.media.attachment( logo.id ).get( 'url' ); - this.setState( { - isPending: false, - logo: { id: logo.id, url: logoUrl }, - } ); - } ); - /* eslint-enable react/no-did-update-set-state */ - } - - if ( - demoStoreNotice && - prevProps.demoStoreNotice !== demoStoreNotice - ) { - /* eslint-disable react/no-did-update-set-state */ - this.setState( { - storeNoticeText: demoStoreNotice, - } ); - /* eslint-enable react/no-did-update-set-state */ - } - } - - async completeStep() { - const { stepIndex } = this.state; - const { actionTask, onComplete } = this.props; - const nextStep = this.getSteps()[ stepIndex + 1 ]; - - if ( nextStep ) { - this.setState( { stepIndex: stepIndex + 1 } ); - } else { - this.setState( { isPending: true } ); - await actionTask( 'appearance' ); - onComplete(); - } - } - - importProducts() { - const { createNotice } = this.props; - this.setState( { isPending: true } ); - - recordEvent( 'tasklist_appearance_import_demo', {} ); - - apiFetch( { - path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/import_sample_products`, - method: 'POST', - } ) - .then( ( result ) => { - if ( result.failed && result.failed.length ) { - createNotice( - 'error', - __( - 'There was an error importing some of the sample products', - 'woocommerce' - ) - ); - } else { - createNotice( - 'success', - __( - 'All sample products have been imported', - 'woocommerce' - ) - ); - } - - this.setState( { isPending: false } ); - this.completeStep(); - } ) - .catch( ( { message } ) => { - createNotice( - 'error', - message || - __( - 'There was an error importing the sample products', - 'woocommerce' - ), - { __unstableHTML: true } - ); - this.setState( { isPending: false } ); - } ); - } - - createHomepage() { - const { createNotice } = this.props; - this.setState( { isPending: true } ); - - recordEvent( 'tasklist_appearance_create_homepage', { - create_homepage: true, - } ); - - apiFetch( { - path: '/wc-admin/onboarding/tasks/create_homepage', - method: 'POST', - } ) - .then( ( response ) => { - createNotice( response.status, response.message, { - actions: response.edit_post_link - ? [ - { - label: __( 'Customize', 'woocommerce' ), - onClick: () => { - queueRecordEvent( - 'tasklist_appearance_customize_homepage', - {} - ); - window.location = `${ response.edit_post_link }&wc_onboarding_active_task=appearance`; - }, - }, - ] - : null, - } ); - - this.setState( { isPending: false } ); - this.completeStep(); - } ) - .catch( ( error ) => { - createNotice( 'error', error.message ); - this.setState( { isPending: false } ); - } ); - } - - async updateLogo() { - const { createNotice, task, updateOptions } = this.props; - const { stylesheet, themeMods } = task.additionalData; - const { logo } = this.state; - const updatedThemeMods = { - ...themeMods, - custom_logo: logo ? logo.id : null, - }; - - recordEvent( 'tasklist_appearance_upload_logo' ); - - this.setState( { isUpdatingLogo: true } ); - const update = await updateOptions( { - [ `theme_mods_${ stylesheet }` ]: updatedThemeMods, - } ); - - if ( update.success ) { - this.setState( { isUpdatingLogo: false } ); - createNotice( - 'success', - __( 'Store logo updated successfully', 'woocommerce' ) - ); - this.completeStep(); - } else { - createNotice( 'error', update.message ); - } - } - - async updateNotice() { - const { createNotice, updateOptions } = this.props; - const { storeNoticeText } = this.state; - - recordEvent( 'tasklist_appearance_set_store_notice', { - added_text: Boolean( storeNoticeText.length ), - } ); - - this.setState( { isUpdatingNotice: true } ); - const update = await updateOptions( { - woocommerce_demo_store: storeNoticeText.length ? 'yes' : 'no', - woocommerce_demo_store_notice: storeNoticeText, - } ); - - if ( update.success ) { - this.setState( { isUpdatingNotice: false } ); - createNotice( - 'success', - __( - "🎨 Your store is looking great! Don't forget to continue personalizing it", - 'woocommerce' - ) - ); - this.completeStep(); - } else { - createNotice( 'error', update.message ); - } - } - - getSteps() { - const { isDirty, isPending, logo, storeNoticeText, isUpdatingLogo } = - this.state; - - const steps = [ - { - key: 'import', - label: __( 'Import sample products', 'woocommerce' ), - description: __( - 'We’ll add some products that will make it easier to see what your store looks like', - 'woocommerce' - ), - content: ( - - - - - ), - visible: this.stepVisibility.import, - }, - { - key: 'homepage', - label: __( 'Create a custom homepage', 'woocommerce' ), - description: __( - 'Create a new homepage and customize it to suit your needs', - 'woocommerce' - ), - content: ( - - - - - ), - visible: this.stepVisibility.homepage, - }, - { - key: 'logo', - label: __( 'Upload a logo', 'woocommerce' ), - description: __( - 'Ensure your store is on-brand by adding your logo', - 'woocommerce' - ), - content: isPending ? null : ( - - - this.setState( { isDirty: true, logo: image } ) - } - /> - - - - ), - visible: this.stepVisibility.logo, - }, - { - key: 'notice', - label: __( 'Set a store notice', 'woocommerce' ), - description: __( - 'Optionally display a prominent notice across all pages of your store', - 'woocommerce' - ), - content: ( - - - this.setState( { storeNoticeText: value } ) - } - /> - - - ), - visible: true, - }, - ]; - - return filter( steps, ( step ) => step.visible ); - } - - render() { - const { isPending, stepIndex, isUpdatingLogo, isUpdatingNotice } = - this.state; - const currentStep = this.getSteps()[ stepIndex ].key; - - return ( -
- - - - - -
+const useAppearanceClick = () => { + const onClick = () => { + window.location = getAdminLink( + 'theme-install.php?browse=block-themes' ); - } -} + }; -const AppearanceWrapper = compose( - withSelect( ( select ) => { - const { getOption } = select( OPTIONS_STORE_NAME ); + return { onClick }; +}; - return { - demoStoreNotice: getOption( 'woocommerce_demo_store_notice' ), - }; - } ), - withDispatch( ( dispatch ) => { - const { createNotice } = dispatch( 'core/notices' ); - const { updateOptions } = dispatch( OPTIONS_STORE_NAME ); - const { actionTask } = dispatch( ONBOARDING_STORE_NAME ); - - return { - actionTask, - createNotice, - updateOptions, - }; - } ) -)( Appearance ); +const AppearanceFill = () => { + const { onClick } = useAppearanceClick(); + return ( + + { ( { defaultTaskItem: DefaultTaskItem } ) => ( + + ) } + + ); +}; registerPlugin( 'wc-admin-onboarding-task-appearance', { scope: 'woocommerce-tasks', - render: () => ( - - { ( { onComplete, task } ) => ( - - ) } - - ), + render: () => , } ); diff --git a/plugins/woocommerce-admin/client/utils/derive-wp-admin-background-colours.ts b/plugins/woocommerce-admin/client/utils/derive-wp-admin-background-colours.ts new file mode 100644 index 00000000000..c039678f374 --- /dev/null +++ b/plugins/woocommerce-admin/client/utils/derive-wp-admin-background-colours.ts @@ -0,0 +1,48 @@ +/** + * Helper function that lightens a colour by blending it + * with some percentage of white + */ +function blendWithWhite( hex: string, alpha: number ) { + let r = parseInt( hex.slice( 1, 3 ), 16 ), + g = parseInt( hex.slice( 3, 5 ), 16 ), + b = parseInt( hex.slice( 5, 7 ), 16 ); + + // Blend with white + r = Math.floor( ( 1 - alpha ) * 255 + alpha * r ); + g = Math.floor( ( 1 - alpha ) * 255 + alpha * g ); + b = Math.floor( ( 1 - alpha ) * 255 + alpha * b ); + + // Convert to hex + const newHex = + '#' + + r.toString( 16 ).padStart( 2, '0' ) + + g.toString( 16 ).padStart( 2, '0' ) + + b.toString( 16 ).padStart( 2, '0' ); + + return newHex; +} + +/** + * wp-admin theme colour only include the main colour, + * but in some applications we want to derive a complementary + * background colour that's some percentage lighter than the + * wp-admin theme colour. This is not doable in CSS as it involves + * breaking down the hex colour code and then running calculations on it. + * As of writing, CSS calc can only operate on individual numbers + */ +export const deriveWpAdminBackgroundColours = () => { + const rootStyles = window.getComputedStyle( document.body ); + const wpAdminThemeColor = rootStyles + .getPropertyValue( '--wp-admin-theme-color' ) + .trim(); + + document.documentElement.style.setProperty( + '--wp-admin-theme-color-background-04', + blendWithWhite( wpAdminThemeColor, 0.04 ) + ); + + document.documentElement.style.setProperty( + '--wp-admin-theme-color-background-25', + blendWithWhite( wpAdminThemeColor, 0.25 ) + ); +}; diff --git a/plugins/woocommerce/.wp-env.json b/plugins/woocommerce/.wp-env.json index c2d13bafcdc..e277dcb5211 100644 --- a/plugins/woocommerce/.wp-env.json +++ b/plugins/woocommerce/.wp-env.json @@ -1,40 +1,38 @@ { - "phpVersion": "7.4", - "plugins": [ - "." - ], - "config": { - "JETPACK_AUTOLOAD_DEV": true, - "WP_DEBUG_LOG": true, - "WP_DEBUG_DISPLAY": true, - "ALTERNATE_WP_CRON": true - }, - "mappings": { - "wp-cli.yml": "./tests/wp-cli.yml", - "wp-content/plugins/filter-setter.php": "./tests/e2e-pw/bin/filter-setter.php", - "wp-content/plugins/enable-experimental-features.php": "./tests/e2e-pw/bin/enable-experimental-features.php" - }, - "lifecycleScripts": { - "afterStart": "./tests/e2e-pw/bin/test-env-setup.sh", - "afterClean": "./tests/e2e-pw/bin/test-env-setup.sh" - }, - "env": { - "development": {}, - "tests": { - "port": 8086, - "plugins": [ - ".", - "https://downloads.wordpress.org/plugin/akismet.zip", - "https://github.com/WP-API/Basic-Auth/archive/master.zip", - "https://downloads.wordpress.org/plugin/wp-mail-logging.zip" - ], - "themes": [ - "https://downloads.wordpress.org/theme/twentynineteen.zip" - ], - "config": { - "WP_TESTS_DOMAIN": "localhost", - "ALTERNATE_WP_CRON": false - } - } - } + "phpVersion": "7.4", + "plugins": [ "." ], + "config": { + "JETPACK_AUTOLOAD_DEV": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": true, + "ALTERNATE_WP_CRON": true + }, + "mappings": { + "wp-cli.yml": "./tests/wp-cli.yml", + "wp-content/plugins/filter-setter.php": "./tests/e2e-pw/bin/filter-setter.php", + "wp-content/plugins/test-helper-apis.php": "./tests/e2e-pw/bin/test-helper-apis.php" + }, + "lifecycleScripts": { + "afterStart": "./tests/e2e-pw/bin/test-env-setup.sh", + "afterClean": "./tests/e2e-pw/bin/test-env-setup.sh" + }, + "env": { + "development": {}, + "tests": { + "port": 8086, + "plugins": [ + ".", + "https://downloads.wordpress.org/plugin/akismet.zip", + "https://github.com/WP-API/Basic-Auth/archive/master.zip", + "https://downloads.wordpress.org/plugin/wp-mail-logging.zip" + ], + "themes": [ + "https://downloads.wordpress.org/theme/twentynineteen.zip" + ], + "config": { + "WP_TESTS_DOMAIN": "localhost", + "ALTERNATE_WP_CRON": false + } + } + } } diff --git a/plugins/woocommerce/changelog/add-39528-intro-components b/plugins/woocommerce/changelog/add-39528-intro-components new file mode 100644 index 00000000000..92163218af1 --- /dev/null +++ b/plugins/woocommerce/changelog/add-39528-intro-components @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update intro screen for the new Customize Your Store task diff --git a/plugins/woocommerce/changelog/add-customize-store-best-colours b/plugins/woocommerce/changelog/add-customize-store-best-colours new file mode 100644 index 00000000000..2486bf313fb --- /dev/null +++ b/plugins/woocommerce/changelog/add-customize-store-best-colours @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add customize store AI wizard call for best colour palette suggestions. diff --git a/plugins/woocommerce/changelog/add-customize-store-font-pairing b/plugins/woocommerce/changelog/add-customize-store-font-pairing new file mode 100644 index 00000000000..e5724ead763 --- /dev/null +++ b/plugins/woocommerce/changelog/add-customize-store-font-pairing @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add customize store AI wizard call for font pairing suggestion diff --git a/plugins/woocommerce/changelog/add-hpos_by_default b/plugins/woocommerce/changelog/add-hpos_by_default new file mode 100644 index 00000000000..983f44d074b --- /dev/null +++ b/plugins/woocommerce/changelog/add-hpos_by_default @@ -0,0 +1,4 @@ +Significance: major +Type: enhancement + +Enable HPOS by default for new installs. diff --git a/plugins/woocommerce/changelog/add-regression-test-user-meta b/plugins/woocommerce/changelog/add-regression-test-user-meta new file mode 100644 index 00000000000..52ede0eec5e --- /dev/null +++ b/plugins/woocommerce/changelog/add-regression-test-user-meta @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Adds regression test for https://github.com/woocommerce/woocommerce/pull/40221. + + diff --git a/plugins/woocommerce/changelog/dev-add-assembler-e2e-tests b/plugins/woocommerce/changelog/dev-add-assembler-e2e-tests new file mode 100644 index 00000000000..c8e94e72597 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-add-assembler-e2e-tests @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Add some basic E2E tests for Assembler Hub diff --git a/plugins/woocommerce/changelog/dev-update-address-type-comments b/plugins/woocommerce/changelog/dev-update-address-type-comments new file mode 100644 index 00000000000..bb1a4fd5a34 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-update-address-type-comments @@ -0,0 +1,5 @@ +Significance: patch +Type: tweak +Comment: This change only tweaks a few comments. + + diff --git a/plugins/woocommerce/changelog/fix-cys-visual-tweaks-12-sep b/plugins/woocommerce/changelog/fix-cys-visual-tweaks-12-sep new file mode 100644 index 00000000000..44d77d37c32 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cys-visual-tweaks-12-sep @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Addressed visual tweaks for CYS in response to feedback from 12th Sept diff --git a/plugins/woocommerce/changelog/fix-hpos-pending-sync-count-order-types b/plugins/woocommerce/changelog/fix-hpos-pending-sync-count-order-types new file mode 100644 index 00000000000..80db4ec992f --- /dev/null +++ b/plugins/woocommerce/changelog/fix-hpos-pending-sync-count-order-types @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Address missing order type handling in HPOS compatibility mode sync. diff --git a/plugins/woocommerce/changelog/fix-metadata-not-updated-on-order-update b/plugins/woocommerce/changelog/fix-metadata-not-updated-on-order-update new file mode 100644 index 00000000000..0f9f0a5e20d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-metadata-not-updated-on-order-update @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix changes in order custom fields made from admin not being applied when using the order Update button with HPOS active. diff --git a/plugins/woocommerce/changelog/update-address-cys-ui-feedback b/plugins/woocommerce/changelog/update-address-cys-ui-feedback new file mode 100644 index 00000000000..241ec85e6ef --- /dev/null +++ b/plugins/woocommerce/changelog/update-address-cys-ui-feedback @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix CYS UI issues diff --git a/plugins/woocommerce/changelog/update-appearance-task b/plugins/woocommerce/changelog/update-appearance-task new file mode 100644 index 00000000000..dc069582853 --- /dev/null +++ b/plugins/woocommerce/changelog/update-appearance-task @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Replace Personalize Your Store task with Choose Your Theme diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-11.1.1 b/plugins/woocommerce/changelog/update-woocommerce-blocks-11.1.1 new file mode 100644 index 00000000000..44356b9d68e --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-11.1.1 @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Bump Woo Blocks 11.1.1 diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 238800289bb..9351d5146e4 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -23,7 +23,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.6.3", - "woocommerce/woocommerce-blocks": "11.1.0" + "woocommerce/woocommerce-blocks": "11.1.1" }, "require-dev": { "automattic/jetpack-changelogger": "^3.3.0", diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index b9c75cc9229..ea1924d7ec6 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cc5c4172d1078ecf041bdf3240a8c4e1", + "content-hash": "e5800331e1c25cd08ad877b665da724f", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -1004,16 +1004,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "11.1.0", + "version": "11.1.1", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "9a9749f72c16bbb9cd75e23061f6b19dec01c262" + "reference": "81bff865d7fdd7de40a4da8fc49539a124ac1863" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/9a9749f72c16bbb9cd75e23061f6b19dec01c262", - "reference": "9a9749f72c16bbb9cd75e23061f6b19dec01c262", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/81bff865d7fdd7de40a4da8fc49539a124ac1863", + "reference": "81bff865d7fdd7de40a4da8fc49539a124ac1863", "shasum": "" }, "require": { @@ -1062,9 +1062,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v11.1.0" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v11.1.1" }, - "time": "2023-09-12T13:56:36+00:00" + "time": "2023-09-20T10:14:34+00:00" } ], "packages-dev": [ diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php index cd39ca9ef6d..8dc5c20e121 100644 --- a/plugins/woocommerce/includes/class-wc-checkout.php +++ b/plugins/woocommerce/includes/class-wc-checkout.php @@ -1291,7 +1291,7 @@ class WC_Checkout { * Get a posted address field after sanitization and validation. * * @param string $key Field key. - * @param string $type Type of address. Available options: 'billing' or 'shipping'. + * @param string $type Type of address; 'billing' or 'shipping'. * @return string */ public function get_posted_address_data( $key, $type = 'billing' ) { diff --git a/plugins/woocommerce/includes/class-wc-customer.php b/plugins/woocommerce/includes/class-wc-customer.php index 06b262e5439..9074442a2f7 100644 --- a/plugins/woocommerce/includes/class-wc-customer.php +++ b/plugins/woocommerce/includes/class-wc-customer.php @@ -454,18 +454,27 @@ class WC_Customer extends WC_Legacy_Customer { * * @since 3.0.0 * @param string $prop Name of prop to get. - * @param string $address billing or shipping. - * @param string $context What the value is for. Valid values are 'view' and 'edit'. What the value is for. Valid values are view and edit. + * @param string $address_type Type of address; 'billing' or 'shipping'. + * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return mixed */ - protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { + protected function get_address_prop( $prop, $address_type = 'billing', $context = 'view' ) { $value = null; - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { - $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; + if ( array_key_exists( $prop, $this->data[ $address_type ] ) ) { + $value = isset( $this->changes[ $address_type ][ $prop ] ) ? $this->changes[ $address_type ][ $prop ] : $this->data[ $address_type ][ $prop ]; if ( 'view' === $context ) { - $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); + /** + * Filter: 'woocommerce_customer_get_[billing|shipping]_[prop]' + * + * Allow developers to change the returned value for any customer address property. + * + * @since 3.6.0 + * @param string $value The address property value. + * @param WC_Customer $customer The customer object being read. + */ + $value = apply_filters( $this->get_hook_prefix() . $address_type . '_' . $prop, $value, $this ); } } return $value; @@ -920,18 +929,18 @@ class WC_Customer extends WC_Legacy_Customer { * Sets a prop for a setter method. * * @since 3.0.0 - * @param string $prop Name of prop to set. - * @param string $address Name of address to set. billing or shipping. - * @param mixed $value Value of the prop. + * @param string $prop Name of prop to set. + * @param string $address_type Type of address; 'billing' or 'shipping'. + * @param mixed $value Value of the prop. */ - protected function set_address_prop( $prop, $address, $value ) { - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + protected function set_address_prop( $prop, $address_type, $value ) { + if ( array_key_exists( $prop, $this->data[ $address_type ] ) ) { if ( true === $this->object_read ) { - if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { - $this->changes[ $address ][ $prop ] = $value; + if ( $value !== $this->data[ $address_type ][ $prop ] || ( isset( $this->changes[ $address_type ] ) && array_key_exists( $prop, $this->changes[ $address_type ] ) ) ) { + $this->changes[ $address_type ][ $prop ] = $value; } } else { - $this->data[ $address ][ $prop ] = $value; + $this->data[ $address_type ][ $prop ] = $value; } } } diff --git a/plugins/woocommerce/includes/class-wc-form-handler.php b/plugins/woocommerce/includes/class-wc-form-handler.php index 21d5744ac11..972818b4cda 100644 --- a/plugins/woocommerce/includes/class-wc-form-handler.php +++ b/plugins/woocommerce/includes/class-wc-form-handler.php @@ -103,13 +103,13 @@ class WC_Form_Handler { return; } - $load_address = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing'; + $address_type = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing'; - if ( ! isset( $_POST[ $load_address . '_country' ] ) ) { + if ( ! isset( $_POST[ $address_type . '_country' ] ) ) { return; } - $address = WC()->countries->get_address_fields( wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ), $load_address . '_' ); + $address = WC()->countries->get_address_fields( wc_clean( wp_unslash( $_POST[ $address_type . '_country' ] ) ), $address_type . '_' ); foreach ( $address as $key => $field ) { if ( ! isset( $field['type'] ) ) { @@ -138,7 +138,7 @@ class WC_Form_Handler { foreach ( $field['validate'] as $rule ) { switch ( $rule ) { case 'postcode': - $country = wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ); + $country = wc_clean( wp_unslash( $_POST[ $address_type . '_country' ] ) ); $value = wc_format_postcode( $value, $country ); if ( '' !== $value && ! WC_Validation::is_postcode( $value, $country ) ) { @@ -191,12 +191,13 @@ class WC_Form_Handler { * * Allow developers to add custom validation logic and throw an error to prevent save. * + * @since 3.6.0 * @param int $user_id User ID being saved. - * @param string $load_address Type of address e.g. billing or shipping. + * @param string $address_type Type of address; 'billing' or 'shipping'. * @param array $address The address fields. - * @param WC_Customer $customer The customer object being saved. @since 3.6.0 + * @param WC_Customer $customer The customer object being saved. */ - do_action( 'woocommerce_after_save_address_validation', $user_id, $load_address, $address, $customer ); + do_action( 'woocommerce_after_save_address_validation', $user_id, $address_type, $address, $customer ); if ( 0 < wc_notice_count( 'error' ) ) { return; @@ -206,7 +207,16 @@ class WC_Form_Handler { wc_add_notice( __( 'Address changed successfully.', 'woocommerce' ) ); - do_action( 'woocommerce_customer_save_address', $user_id, $load_address ); + /** + * Hook: woocommerce_customer_save_address. + * + * Fires after a customer address has been saved. + * + * @since 3.6.0 + * @param int $user_id User ID being saved. + * @param string $address_type Type of address; 'billing' or 'shipping'. + */ + do_action( 'woocommerce_customer_save_address', $user_id, $address_type ); wp_safe_redirect( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ); exit; diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 8176e7bc93a..c451fee1313 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -18,6 +18,7 @@ use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchro use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; +use Automattic\WooCommerce\Utilities\OrderUtil; defined( 'ABSPATH' ) || exit; @@ -256,6 +257,7 @@ class WC_Install { public static function init() { add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); add_action( 'init', array( __CLASS__, 'manual_database_update' ), 20 ); + add_action( 'woocommerce_newly_installed', array( __CLASS__, 'maybe_enable_hpos' ), 20 ); add_action( 'admin_init', array( __CLASS__, 'wc_admin_db_update_notice' ) ); add_action( 'admin_init', array( __CLASS__, 'add_admin_note_after_page_created' ) ); add_action( 'woocommerce_run_update_callback', array( __CLASS__, 'run_update_callback' ) ); @@ -878,6 +880,51 @@ class WC_Install { } } + /** + * Enable HPOS by default for new shops. + * + * @since 8.2.0 + */ + public static function maybe_enable_hpos() { + if ( self::should_enable_hpos_for_new_shop() ) { + $feature_controller = wc_get_container()->get( FeaturesController::class ); + $feature_controller->change_feature_enable( 'custom_order_tables', true ); + } + } + + /** + * Checks whether HPOS should be enabled for new shops. + * + * @return bool + */ + private static function should_enable_hpos_for_new_shop() { + if ( ! did_action( 'woocommerce_init' ) && ! doing_action( 'woocommerce_init' ) ) { + return false; + } + + $feature_controller = wc_get_container()->get( FeaturesController::class ); + + if ( OrderUtil::custom_orders_table_usage_is_enabled() ) { + return true; + } + + if ( ! empty( wc_get_orders( array( 'limit' => 1 ) ) ) ) { + return false; + } + + $plugin_compat_info = $feature_controller->get_compatible_plugins_for_feature( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, true ); + if ( ! empty( $plugin_compat_info['incompatible'] ) || ! empty( $plugin_compat_info['uncertain'] ) ) { + return false; + } + + /** + * Filter to enable HPOS by default for new shops. + * + * @since 8.2.0 + */ + return apply_filters( 'woocommerce_enable_hpos_by_default_for_new_shops', true ); + } + /** * Delete obsolete notes. */ @@ -1167,7 +1214,9 @@ class WC_Install { $feature_controller = wc_get_container()->get( FeaturesController::class ); $hpos_enabled = - $feature_controller->feature_is_enabled( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION ) || $feature_controller->feature_is_enabled( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ); + $feature_controller->feature_is_enabled( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION ) || $feature_controller->feature_is_enabled( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) || + self::should_enable_hpos_for_new_shop() + ; $hpos_table_schema = $hpos_enabled ? wc_get_container()->get( OrdersTableDataStore::class )->get_database_schema() : ''; $tables = " diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php index 6a1874bcee9..f3550ec7beb 100644 --- a/plugins/woocommerce/includes/class-wc-order.php +++ b/plugins/woocommerce/includes/class-wc-order.php @@ -557,18 +557,27 @@ class WC_Order extends WC_Abstract_Order { * * @since 3.0.0 * @param string $prop Name of prop to get. - * @param string $address billing or shipping. + * @param string $address_type Type of address; 'billing' or 'shipping'. * @param string $context What the value is for. Valid values are view and edit. * @return mixed */ - protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { + protected function get_address_prop( $prop, $address_type = 'billing', $context = 'view' ) { $value = null; - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { - $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; + if ( array_key_exists( $prop, $this->data[ $address_type ] ) ) { + $value = isset( $this->changes[ $address_type ][ $prop ] ) ? $this->changes[ $address_type ][ $prop ] : $this->data[ $address_type ][ $prop ]; if ( 'view' === $context ) { - $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); + /** + * Filter: 'woocommerce_order_get_[billing|shipping]_[prop]' + * + * Allow developers to change the returned value for any order address property. + * + * @since 3.6.0 + * @param string $value The address property value. + * @param WC_Order $order The order object being read. + */ + $value = apply_filters( $this->get_hook_prefix() . $address_type . '_' . $prop, $value, $this ); } } return $value; @@ -896,11 +905,20 @@ class WC_Order extends WC_Abstract_Order { * Note: Merges raw data with get_prop data so changes are returned too. * * @since 2.4.0 - * @param string $type Billing or shipping. Anything else besides 'billing' will return shipping address. + * @param string $address_type Type of address; 'billing' or 'shipping'. * @return array The stored address after filter. */ - public function get_address( $type = 'billing' ) { - return apply_filters( 'woocommerce_get_order_address', array_merge( $this->data[ $type ], $this->get_prop( $type, 'view' ) ), $type, $this ); + public function get_address( $address_type = 'billing' ) { + /** + * Filter: 'woocommerce_get_order_address' + * + * Allow developers to change the returned value for an order's billing or shipping address. + * + * @since 2.4.0 + * @param array $address_data The raw address data merged with the data from get_prop. + * @param string $address_type Type of address; 'billing' or 'shipping'. + */ + return apply_filters( 'woocommerce_get_order_address', array_merge( $this->data[ $address_type ], $this->get_prop( $address_type, 'view' ) ), $address_type, $this ); } /** @@ -1067,17 +1085,17 @@ class WC_Order extends WC_Abstract_Order { * * @since 3.0.0 * @param string $prop Name of prop to set. - * @param string $address Name of address to set. billing or shipping. + * @param string $address_type Type of address; 'billing' or 'shipping'. * @param mixed $value Value of the prop. */ - protected function set_address_prop( $prop, $address, $value ) { - if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + protected function set_address_prop( $prop, $address_type, $value ) { + if ( array_key_exists( $prop, $this->data[ $address_type ] ) ) { if ( true === $this->object_read ) { - if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { - $this->changes[ $address ][ $prop ] = $value; + if ( $value !== $this->data[ $address_type ][ $prop ] || ( isset( $this->changes[ $address_type ] ) && array_key_exists( $prop, $this->changes[ $address_type ] ) ) ) { + $this->changes[ $address_type ][ $prop ] = $value; } } else { - $this->data[ $address ][ $prop ] = $value; + $this->data[ $address_type ][ $prop ] = $value; } } } diff --git a/plugins/woocommerce/includes/legacy/abstract-wc-legacy-order.php b/plugins/woocommerce/includes/legacy/abstract-wc-legacy-order.php index 8867096c739..3c2c8791b35 100644 --- a/plugins/woocommerce/includes/legacy/abstract-wc-legacy-order.php +++ b/plugins/woocommerce/includes/legacy/abstract-wc-legacy-order.php @@ -329,7 +329,7 @@ abstract class WC_Abstract_Legacy_Order extends WC_Data { /** * Set the customer address. * @param array $address Address data. - * @param string $type billing or shipping. + * @param string $type Type of address; 'billing' or 'shipping'. */ public function set_address( $address, $type = 'billing' ) { foreach ( $address as $key => $value ) { diff --git a/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-orders.php b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-orders.php index 937de6e4d35..a8b65f6ef8a 100644 --- a/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-orders.php +++ b/plugins/woocommerce/includes/legacy/api/v2/class-wc-api-orders.php @@ -761,7 +761,7 @@ class WC_API_Orders extends WC_API_Resource { * * @param WC_Order $order * @param array $posted - * @param string $type + * @param string $type Type of address; 'billing' or 'shipping'. */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { diff --git a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-orders.php b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-orders.php index 4d602a94edb..7ed0870a1fb 100644 --- a/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-orders.php +++ b/plugins/woocommerce/includes/legacy/api/v3/class-wc-api-orders.php @@ -805,7 +805,7 @@ class WC_API_Orders extends WC_API_Resource { * * @param WC_Order $order * @param array $posted - * @param string $type + * @param string $type Type of address; 'billing' or 'shipping'. */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php index 7cf477b7db9..7e65221c8f5 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php @@ -593,9 +593,9 @@ class WC_REST_Orders_V1_Controller extends WC_REST_Posts_Controller { /** * Update address. * - * @param WC_Order $order - * @param array $posted - * @param string $type + * @param WC_Order $order Order object. + * @param array $posted Request data. + * @param string $type Type of address; 'billing' or 'shipping'. */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index d8a0a249ac0..8ee7a0edae1 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -804,7 +804,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { * * @param WC_Order $order Order data. * @param array $posted Posted data. - * @param string $type Address type. + * @param string $type Type of address; 'billing' or 'shipping'. */ protected function update_address( $order, $posted, $type = 'billing' ) { foreach ( $posted as $key => $value ) { diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php index 3a569201d82..4cfcc85e028 100644 --- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php +++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php @@ -165,7 +165,7 @@ class WC_Shortcode_My_Account { /** * Edit address page. * - * @param string $load_address Type of address to load. + * @param string $load_address Type of address; 'billing' or 'shipping'. */ public static function edit_address( $load_address = 'billing' ) { $current_user = wp_get_current_user(); diff --git a/plugins/woocommerce/includes/wc-account-functions.php b/plugins/woocommerce/includes/wc-account-functions.php index 7cefe49d9eb..61ee51d95ba 100644 --- a/plugins/woocommerce/includes/wc-account-functions.php +++ b/plugins/woocommerce/includes/wc-account-functions.php @@ -309,11 +309,9 @@ function wc_get_account_orders_actions( $order ) { * Get account formatted address. * * @since 3.2.0 - * @param string $address_type Address type. - * Accepts: 'billing' or 'shipping'. - * Default to 'billing'. + * @param string $address_type Type of address; 'billing' or 'shipping'. * @param int $customer_id Customer ID. - * Default to 0. + * Defaults to 0. * @return string */ function wc_get_account_formatted_address( $address_type = 'billing', $customer_id = 0 ) { diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php index d82d8df4063..678cd1ac361 100644 --- a/plugins/woocommerce/includes/wc-template-functions.php +++ b/plugins/woocommerce/includes/wc-template-functions.php @@ -3295,7 +3295,7 @@ if ( ! function_exists( 'woocommerce_account_edit_address' ) ) { /** * My Account > Edit address template. * - * @param string $type Address type. + * @param string $type Type of address; 'billing' or 'shipping'. */ function woocommerce_account_edit_address( $type ) { $type = wc_edit_address_i18n( sanitize_title( $type ), true ); diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 361d8dee1d7..d9215cdbb7b 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -112,12 +112,12 @@ class TaskLists { 'CustomizeStore', 'StoreDetails', 'Products', + 'Appearance', 'WooCommercePayments', 'Payments', 'Tax', 'Shipping', 'Marketing', - 'Appearance', ); if ( Features::is_enabled( 'core-profiler' ) ) { diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php index 2b2832bf635..721b3db43e3 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php @@ -14,14 +14,12 @@ use Automattic\WooCommerce\Internal\Admin\WCAdminAssets; class Appearance extends Task { /** - * Constructor - * - * @param TaskList $task_list Parent task list. + * Constructor. */ - public function __construct( $task_list ) { - parent::__construct( $task_list ); - add_action( 'admin_enqueue_scripts', array( $this, 'add_media_scripts' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_return_notice_script' ) ); + public function __construct() { + if ( ! $this->is_complete() ) { + add_action( 'load-theme-install.php', array( $this, 'mark_actioned' ) ); + } } /** @@ -39,13 +37,7 @@ class Appearance extends Task { * @return string */ public function get_title() { - if ( $this->get_parent_option( 'use_completed_title' ) === true ) { - if ( $this->is_complete() ) { - return __( 'You personalized your store', 'woocommerce' ); - } - return __( 'Personalize your store', 'woocommerce' ); - } - return __( 'Personalize my store', 'woocommerce' ); + return __( 'Choose your theme', 'woocommerce' ); } /** @@ -55,7 +47,7 @@ class Appearance extends Task { */ public function get_content() { return __( - 'Add your logo, create a homepage, and start designing your store.', + "Choose a theme that best fits your brand's look and feel, then make it your own. Change the colors, add your logo, and create pages.", 'woocommerce' ); } @@ -70,68 +62,11 @@ class Appearance extends Task { } /** - * Addtional data. + * Action label. * - * @return array + * @return string */ - public function get_additional_data() { - return array( - 'has_homepage' => self::has_homepage(), - 'has_products' => Products::has_products(), - 'stylesheet' => get_option( 'stylesheet' ), - 'theme_mods' => get_theme_mods(), - 'support_custom_logo' => false !== get_theme_support( 'custom-logo' ), - ); - } - - /** - * Add media scripts for image uploader. - */ - public function add_media_scripts() { - if ( ! PageController::is_admin_page() || ! $this->can_view() ) { - return; - } - - wp_enqueue_media(); - } - - - /** - * Adds a return to task list notice when completing the task. - * - * @param string $hook Page hook. - */ - public function possibly_add_return_notice_script( $hook ) { - global $post; - - if ( $hook !== 'post.php' || $post->post_type !== 'page' ) { - return; - } - - if ( $this->is_complete() || ! $this->is_active() ) { - return; - } - - WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-homepage-notice', true ); - } - - /** - * Check if the site has a homepage set up. - */ - public static function has_homepage() { - if ( get_option( 'classic-editor-replace' ) === 'classic' ) { - return true; - } - - $homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false ); - - if ( ! $homepage_id ) { - return false; - } - - $post = get_post( $homepage_id ); - $completed = $post && $post->post_status === 'publish'; - - return $completed; + public function get_action_label() { + return __( 'Choose theme', 'woocommerce' ); } } diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php index 31cd9d09b35..699e132fb06 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php @@ -16,7 +16,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; */ class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { /** - * Type of addresses being migrated, could be billing|shipping. + * Type of addresses being migrated; 'billing' or 'shipping'. * * @var $type */ @@ -25,7 +25,7 @@ class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { /** * PostToOrderAddressTableMigrator constructor. * - * @param string $type Type of addresses being migrated, could be billing|shipping. + * @param string $type Type of address being migrated; 'billing' or 'shipping'. */ public function __construct( $type ) { $this->type = $type; diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php index a956c681f27..a1b77a59169 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php @@ -208,7 +208,6 @@ class Edit { * @return void */ public function handle_order_update() { - global $theorder; if ( ! isset( $this->order ) ) { return; } @@ -233,6 +232,8 @@ class Edit { */ do_action( 'woocommerce_process_shop_order_meta', $this->order->get_id(), $this->order ); + $this->custom_meta_box->handle_metadata_changes($this->order); + // Order updated message. $this->message = 1; diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomMetaBox.php b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomMetaBox.php index 5aae6fd949d..dd44808ca72 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomMetaBox.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/CustomMetaBox.php @@ -242,7 +242,7 @@ class CustomMetaBox { * @return void */ private function handle_add_meta( WC_Order $order, string $meta_key, string $meta_value ) { - $count = 0; + $count = 0; if ( is_protected_meta( $meta_key ) ) { wp_send_json_error( 'protected_meta' ); wp_die(); @@ -409,4 +409,52 @@ class CustomMetaBox { } wp_die( 0 ); } + + /** + * Handle the possible changes in order metadata coming from an order edit page in admin + * (labeled "custom fields" in the UI). + * + * This method expects the $_POST array to contain a 'meta' key that is an associative + * array of [meta item id => [ 'key' => meta item name, 'value' => meta item value ]; + * and also to contain (possibly empty) 'metakeyinput' and 'metavalue' keys. + * + * @param WC_Order $order The order to handle. + */ + public function handle_metadata_changes( $order ) { + $has_meta_changes = false; + + $order_meta = $order->get_meta_data(); + + $order_meta = + array_combine( + array_map( fn( $meta ) => $meta->id, $order_meta ), + $order_meta + ); + + // phpcs:disable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing + + foreach ( ( $_POST['meta'] ?? array() ) as $request_meta_id => $request_meta_data ) { + $request_meta_id = wp_unslash( $request_meta_id ); + $request_meta_key = wp_unslash( $request_meta_data['key'] ); + $request_meta_value = wp_unslash( $request_meta_data['value'] ); + if ( array_key_exists( $request_meta_id, $order_meta ) && + ( $order_meta[ $request_meta_id ]->key !== $request_meta_key || $order_meta[ $request_meta_id ]->value !== $request_meta_value ) ) { + $order->update_meta_data( $request_meta_key, $request_meta_value, $request_meta_id ); + $has_meta_changes = true; + } + } + + $request_new_key = wp_unslash( $_POST['metakeyinput'] ?? '' ); + $request_new_value = wp_unslash( $_POST['metavalue'] ?? '' ); + if ( '' !== $request_new_key ) { + $order->add_meta_data( $request_new_key, $request_new_value ); + $has_meta_changes = true; + } + + // phpcs:enable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing + + if ( $has_meta_changes ) { + $order->save(); + } + } } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index c559b251703..b0f31dcddd9 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -251,12 +251,15 @@ class DataSynchronizer implements BatchProcessorInterface { } if ( $this->custom_orders_table_is_authoritative() ) { - $missing_orders_count_sql = " + $missing_orders_count_sql = $wpdb->prepare( + " SELECT COUNT(1) FROM $wpdb->posts posts INNER JOIN $orders_table orders ON posts.id=orders.id WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "' AND orders.status not in ( 'auto-draft' ) -"; + AND orders.type IN ($order_post_type_placeholder)", + $order_post_types + ); $operator = '>'; } else { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared. @@ -374,13 +377,16 @@ ORDER BY posts.ID ASC", // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare break; case self::ID_TYPE_MISSING_IN_POSTS_TABLE: - $sql = " + $sql = $wpdb->prepare( + " SELECT posts.ID FROM $wpdb->posts posts INNER JOIN $orders_table orders ON posts.id=orders.id WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "' AND orders.status not in ( 'auto-draft' ) -ORDER BY posts.id ASC -"; +AND orders.type IN ($order_post_type_placeholders) +ORDER BY posts.id ASC", + $order_post_types + ); break; case self::ID_TYPE_DIFFERENT_UPDATE_DATE: $operator = $this->custom_orders_table_is_authoritative() ? '>' : '<'; diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index dc7d37f2bc0..71d66846787 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -537,7 +537,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements /** * Helper function to get alias for address table, this is used in select query. * - * @param string $type Address type. + * @param string $type Type of address; 'billing' or 'shipping'. * * @return string Alias. */ @@ -1665,7 +1665,7 @@ FROM $order_meta_table /** * Helper method to generate join and select query for address table. * - * @param string $address_type Type of address. Typically will be `billing` or `shipping`. + * @param string $address_type Type of address; 'billing' or 'shipping'. * @param string $order_table_alias Alias of order table to use. * @param string $address_table_alias Alias for address table to use. * diff --git a/plugins/woocommerce/tests/e2e-pw/bin/enable-experimental-features.php b/plugins/woocommerce/tests/e2e-pw/bin/enable-experimental-features.php deleted file mode 100644 index 327cb6994e5..00000000000 --- a/plugins/woocommerce/tests/e2e-pw/bin/enable-experimental-features.php +++ /dev/null @@ -1,13 +0,0 @@ - 'POST', + 'callback' => 'update_feature_flags', + 'permission_callback' => 'is_allowed', + ) + ); + + register_rest_route( + 'e2e-feature-flags', + '/reset', + array( + 'methods' => 'GET', + 'callback' => 'reset_feature_flags', + 'permission_callback' => 'is_allowed', + ) + ); + + register_rest_route( + 'e2e-options', + '/update', + array( + 'methods' => 'POST', + 'callback' => 'api_update_option', + 'permission_callback' => 'is_allowed', + ) + ); +} + +add_action( 'rest_api_init', 'register_helper_api' ); + +/** + * Update feature flags + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ +function update_feature_flags( WP_REST_Request $request ) { + $features = get_option( 'e2e_feature_flags', array() ); + $new_features = json_decode( $request->get_body(), true ); + + if ( is_array( $new_features ) ) { + $features = array_merge( $features, $new_features ); + update_option( 'e2e_feature_flags', $features ); + return new WP_REST_Response( 'Feature flags updated', 200 ); + } + + return new WP_REST_Response( 'Invalid request body', 400 ); +} + +/** + * Reset feature flags + * @return WP_REST_Response + */ +function reset_feature_flags() { + delete_option( 'e2e_feature_flags' ); + return new WP_REST_Response( 'Feature flags reset', 200 ); +} + +/** + * Enable experimental features + * @param array $features Array of features. + * @return array + */ +function enable_experimental_features( $features ) { + $stored_features = get_option( 'e2e_feature_flags', array() ); + + // We always enable this for tests at the moment. + $features['product-variation-management'] = true; + + return array_merge( $features, $stored_features ); +} + +add_filter( 'woocommerce_admin_get_feature_config', 'enable_experimental_features' ); + +/** + * Update a WordPress option. + * @param WP_REST_Request $request + * @return WP_REST_Response + */ +function api_update_option( WP_REST_Request $request ) { + $option_name = sanitize_text_field( $request['option_name'] ); + $option_value = sanitize_text_field( $request['option_value'] ); + + if ( update_option( $option_name, $option_value ) ) { + return new WP_REST_Response( 'Option updated', 200 ); + } + + return new WP_REST_Response( 'Invalid request body', 400 ); +} + +/** + * Check if user is admin + * @return bool + */ +function is_allowed() { + return current_user_can( 'manage_options' ); +} diff --git a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js index 50b74fff794..452858bc320 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/admin-tasks/payment.spec.js @@ -1,6 +1,5 @@ const { test, expect } = require( '@playwright/test' ); const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default; -const { features } = require( '../../utils' ); test.describe( 'Payment setup task', () => { test.use( { storageState: process.env.ADMINSTATE } ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler-hub.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler-hub.spec.js new file mode 100644 index 00000000000..d7b6a6a18c8 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/tests/customize-store/assembler-hub.spec.js @@ -0,0 +1,65 @@ +const { test, expect, request } = require( '@playwright/test' ); +const { features } = require( '../../utils' ); +const { activateTheme } = require( '../../utils/themes' ); +const { setOption } = require( '../../utils/options' ); + +const ASSEMBLER_HUB_URL = + '/wp-admin/admin.php?page=wc-admin&path=%2Fcustomize-store%2Fassembler-hub'; + +test.describe( 'Store owner can view Assembler Hub for store customization', () => { + test.use( { storageState: process.env.ADMINSTATE } ); + + test.beforeAll( async ( { baseURL } ) => { + // In some environments the tour blocks clicking other elements. + await setOption( + request, + baseURL, + 'woocommerce_customize_store_onboarding_tour_hidden', + 'yes' + ); + + await features.setFeatureFlag( + request, + baseURL, + 'customize-store', + true + ); + + // Need a block enabled theme to test + await activateTheme( 'twentytwentythree' ); + } ); + + test.afterAll( async ( { baseURL } ) => { + await features.resetFeatureFlags( request, baseURL ); + + // Reset theme back to twentynineteen + await activateTheme( 'twentynineteen' ); + + // Reset tour to visible. + await setOption( + request, + baseURL, + 'woocommerce_customize_store_onboarding_tour_hidden', + 'no' + ); + } ); + + test( 'Can view the Assembler Hub page', async ( { page } ) => { + await page.goto( ASSEMBLER_HUB_URL ); + const locator = page.locator( 'h1:visible' ); + await expect( locator ).toHaveText( "Let's get creative" ); + } ); + + test( 'Visiting change header should show a list of block patterns to choose from', async ( { + page, + } ) => { + await page.goto( ASSEMBLER_HUB_URL ); + await page.click( 'text=Change your header' ); + + const locator = page.locator( + '.block-editor-block-patterns-list__list-item' + ); + + await expect( locator ).toHaveCount( 4 ); + } ); +} ); diff --git a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-edit.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-edit.spec.js index 94ae4cef3ac..270f93ba851 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-edit.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/merchant/order-edit.spec.js @@ -86,6 +86,75 @@ test.describe( 'Edit order', () => { '2018-12-14' ); } ); + + test( 'can load billing details', async ( { page, baseURL } ) => { + let customerId = 0; + + const api = new wcApi( { + url: baseURL, + consumerKey: process.env.CONSUMER_KEY, + consumerSecret: process.env.CONSUMER_SECRET, + version: 'wc/v3', + } ); + + await api + .post( 'customers', { + email: 'archie123@email.addr', + first_name: 'Archie', + last_name: 'Greenback', + username: 'big.archie', + billing: { + first_name: 'Archibald', + last_name: 'Greenback', + company: 'Automattic', + country: 'US', + address_1: 'address1', + address_2: 'address2', + city: 'San Francisco', + state: 'CA', + postcode: '94107', + phone: '123456789', + email: 'archie123@email.addr', + } + } ) + .then( ( response ) => { + customerId = response.data.id; + } ); + + // Open our test order and select the customer we just created. + await page.goto( `wp-admin/post.php?post=${orderId}&action=edit` ); + + // Simulate the ajax `woocommerce_get_customer_details` call normally done inside meta-boxes-order.js. + const response = await page.evaluate( + async ( customerId ) => { + const simulateCustomerDetailsCall = new Promise( ( resolve ) => { + jQuery.ajax( { + url: woocommerce_admin_meta_boxes.ajax_url, + data: { + user_id : customerId, + action : 'woocommerce_get_customer_details', + security: woocommerce_admin_meta_boxes.get_customer_details_nonce + }, + type: 'POST', + success: function( response ) { + resolve( response ); + } + } ); + } ); + + return await simulateCustomerDetailsCall; + }, + customerId + ); + + // Response should contain billing address info, but should not contain user meta data. + expect( 'billing' in response ).toBeTruthy(); + expect( response.billing.first_name ).toContain( 'Archibald' ); + expect( response.meta_data ).toBeUndefined(); + + // Clean-up. + await api.delete( `customers/${ customerId }`, { force: true } ); + } ); } ); test.describe( 'Edit order > Downloadable product permissions', () => { diff --git a/plugins/woocommerce/tests/e2e-pw/utils/features.js b/plugins/woocommerce/tests/e2e-pw/utils/features.js index 4aa3ecff2b9..db75f1b003b 100644 --- a/plugins/woocommerce/tests/e2e-pw/utils/features.js +++ b/plugins/woocommerce/tests/e2e-pw/utils/features.js @@ -1,14 +1,41 @@ -function is_enabled( feature ) { - const phase = process.env.WC_ADMIN_PHASE; - let config = 'development.json'; - if ( ![ 'core','developer' ].includes( phase ) ) { - config = 'core.json'; - } +const { encodeCredentials } = require( './plugin-utils' ); - const features = require( `../../../client/admin/config/${config}` ).features; - return features[ feature ] && features[ feature ] === true; -} +const setFeatureFlag = async ( request, baseURL, flagName, enable ) => { + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( + 'admin', + 'password' + ) }`, + cookie: '', + }, + } ); + + await apiContext.post( '/wp-json/e2e-feature-flags/update', { + failOnStatusCode: true, + data: { [ flagName ]: enable }, + } ); +}; + +const resetFeatureFlags = async ( request, baseURL ) => { + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( + 'admin', + 'password' + ) }`, + cookie: '', + }, + } ); + + await apiContext.get( '/wp-json/e2e-feature-flags/reset', { + failOnStatusCode: true, + } ); +}; module.exports = { - is_enabled -} \ No newline at end of file + setFeatureFlag, + resetFeatureFlags, +}; diff --git a/plugins/woocommerce/tests/e2e-pw/utils/options.js b/plugins/woocommerce/tests/e2e-pw/utils/options.js new file mode 100644 index 00000000000..1254c94ec47 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/utils/options.js @@ -0,0 +1,24 @@ +import { encodeCredentials } from './plugin-utils'; + +export const setOption = async ( + request, + baseURL, + optionName, + optionValue +) => { + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( + 'admin', + 'password' + ) }`, + cookie: '', + }, + } ); + + await apiContext.post( '/wp-json/e2e-options/update', { + failOnStatusCode: true, + data: { option_name: optionName, option_value: optionValue }, + } ); +}; diff --git a/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js b/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js index a3be326a9a5..9b7e4ca97e3 100644 --- a/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js +++ b/plugins/woocommerce/tests/e2e-pw/utils/plugin-utils.js @@ -12,7 +12,7 @@ const execAsync = promisify( require( 'child_process' ).exec ); * @param {string} password * @returns Base64-encoded string */ -const encodeCredentials = ( username, password ) => { +export const encodeCredentials = ( username, password ) => { return Buffer.from( `${ username }:${ password }` ).toString( 'base64' ); }; diff --git a/plugins/woocommerce/tests/e2e-pw/utils/themes.js b/plugins/woocommerce/tests/e2e-pw/utils/themes.js new file mode 100644 index 00000000000..7a8b9a9a456 --- /dev/null +++ b/plugins/woocommerce/tests/e2e-pw/utils/themes.js @@ -0,0 +1,16 @@ +const { exec } = require( 'node:child_process' ); + +export const activateTheme = ( themeName ) => { + return new Promise( ( resolve, reject ) => { + const command = `wp-env run tests-cli wp theme activate ${ themeName }`; + + exec( command, ( error, stdout, stderr ) => { + if ( error ) { + console.error( `Error executing command: ${ error }` ); + return reject( error ); + } + + resolve( stdout ); + } ); + } ); +}; diff --git a/plugins/woocommerce/tests/e2e/docker/initialize.sh b/plugins/woocommerce/tests/e2e/docker/initialize.sh index 4772fccbd17..7a3ce96b740 100755 --- a/plugins/woocommerce/tests/e2e/docker/initialize.sh +++ b/plugins/woocommerce/tests/e2e/docker/initialize.sh @@ -30,5 +30,5 @@ wp plugin activate filter-setter # initialize pretty permalinks wp rewrite structure /%postname%/ -# Activate our Enable Experimental Features utility. -wp plugin activate enable-experimental-features +# Activate our helper APIs plugin. +wp plugin activate test-helper-apis diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php index 93ab1a62aba..2180f246105 100644 --- a/plugins/woocommerce/tests/legacy/bootstrap.php +++ b/plugins/woocommerce/tests/legacy/bootstrap.php @@ -226,6 +226,10 @@ class WC_Unit_Tests_Bootstrap { define( 'WC_REMOVE_ALL_DATA', true ); include $this->plugin_dir . '/uninstall.php'; + if ( ! getenv( 'HPOS' ) ) { + add_filter( 'woocommerce_enable_hpos_by_default_for_new_shops', '__return_false' ); + } + WC_Install::install(); // Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374.