Merge branch 'trunk' into feature/34904-marketing-introduction-banner

This commit is contained in:
Gan Eng Chin 2023-03-19 20:09:23 +08:00
commit 7cc8e065f9
No known key found for this signature in database
GPG Key ID: 94D5D972860ADB01
87 changed files with 3771 additions and 1873 deletions

View File

@ -51,7 +51,7 @@
"sass": "^1.49.9",
"sass-loader": "^10.2.1",
"syncpack": "^9.8.4",
"turbo": "^1.7.0",
"turbo": "^1.8.3",
"typescript": "^4.8.3",
"url-loader": "^1.1.2",
"webpack": "^5.70.0"

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add a product header component to the blocks interface

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add tests around product block editor tabs

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add new pricing block to the product editor package.

View File

@ -36,6 +36,7 @@
"@woocommerce/data": "workspace:^4.1.0",
"@woocommerce/navigation": "workspace:^8.1.0",
"@woocommerce/number": "workspace:*",
"@woocommerce/settings": "^1.0.0",
"@woocommerce/tracks": "workspace:^1.3.0",
"@wordpress/block-editor": "^9.8.0",
"@wordpress/blocks": "^12.3.0",

View File

@ -12,7 +12,6 @@ import { Product } from '@woocommerce/data';
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { EntityProvider } from '@wordpress/core-data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
@ -30,9 +29,11 @@ import { BlockEditor } from '../block-editor';
import { initBlocks } from './init-blocks';
initBlocks();
export type ProductEditorSettings = Partial<
EditorSettings & EditorBlockListSettings
>;
type EditorProps = {
product: Product;
settings: ProductEditorSettings | undefined;
@ -46,7 +47,12 @@ export function Editor( { product, settings }: EditorProps ) {
<FullscreenMode isActive={ false } />
<SlotFillProvider>
<InterfaceSkeleton
header={ <Header title={ product.name } /> }
header={
<Header
productId={ product.id }
productName={ product.name }
/>
}
content={
<BlockEditor
settings={ settings }

View File

@ -1,12 +1,20 @@
/**
* External dependencies
*/
import { registerCoreBlocks } from '@wordpress/block-library';
/**
* Internal dependencies
*/
import { init as initName } from '../details-name-block';
import { init as initSection } from '../section';
import { init as initTab } from '../tab';
import { init as initPricing } from '../pricing-block';
export const initBlocks = () => {
registerCoreBlocks();
initName();
initSection();
initTab();
initPricing();
};

View File

@ -1,14 +1,68 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Product } from '@woocommerce/data';
import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { navigateTo, getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { AUTO_DRAFT_NAME, getHeaderTitle } from '../../utils';
export type HeaderProps = {
title: string;
productId: number;
productName: string;
};
export function Header( { title }: HeaderProps ) {
export function Header( { productId, productName }: HeaderProps ) {
const { isProductLocked, isSaving, editedProductName } = useSelect(
( select ) => {
const { isSavingEntityRecord, getEditedEntityRecord } =
select( 'core' );
const { isPostSavingLocked } = select( 'core/editor' );
const product: Product = getEditedEntityRecord(
'postType',
'product',
productId
);
return {
isProductLocked: isPostSavingLocked(),
isSaving: isSavingEntityRecord(
'postType',
'product',
productId
),
editedProductName: product?.name,
};
},
[ productId ]
);
const isDisabled = isProductLocked || isSaving;
const isCreating = productName === AUTO_DRAFT_NAME;
const { saveEditedEntityRecord } = useDispatch( 'core' );
function handleSave() {
saveEditedEntityRecord< Product >(
'postType',
'product',
productId
).then( ( response ) => {
if ( isCreating ) {
navigateTo( {
url: getNewPath( {}, `/product/${ response.id }` ),
} );
}
} );
}
return (
<div
className="woocommerce-product-header"
@ -16,7 +70,22 @@ export function Header( { title }: HeaderProps ) {
aria-label={ __( 'Product Editor top bar.', 'woocommerce' ) }
tabIndex={ -1 }
>
<h1 className="woocommerce-product-header__title">{ title }</h1>
<h1 className="woocommerce-product-header__title">
{ getHeaderTitle( editedProductName, productName ) }
</h1>
<div className="woocommerce-product-header__actions">
<Button
onClick={ handleSave }
variant="primary"
isBusy={ isSaving }
disabled={ isDisabled }
>
{ isCreating
? __( 'Add', 'woocommerce' )
: __( 'Save', 'woocommerce' ) }
</Button>
</div>
</div>
);
}

View File

@ -3,4 +3,8 @@
display: flex;
align-items: center;
padding: 0 $gap;
&__actions {
margin-left: auto;
}
}

View File

@ -0,0 +1,29 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-pricing",
"description": "A product price block with currency display.",
"title": "Product pricing",
"category": "widgets",
"keywords": [ "products", "price" ],
"textdomain": "default",
"attributes": {
"name": {
"type": "string"
},
"label": {
"type": "string"
},
"showPricingSection": {
"type": "boolean"
}
},
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
}
}

View File

@ -0,0 +1,89 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createElement, useContext, Fragment } from '@wordpress/element';
import interpolateComponents from '@automattic/interpolate-components';
import { Link } from '@woocommerce/components';
import { useBlockProps } from '@wordpress/block-editor';
import { useEntityProp } from '@wordpress/core-data';
import { BlockAttributes } from '@wordpress/blocks';
import { CurrencyContext } from '@woocommerce/currency';
import { getSetting } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
import {
BaseControl,
// @ts-expect-error `__experimentalInputControl` does exist.
__experimentalInputControl as InputControl,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { formatCurrencyDisplayValue } from '../../utils';
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
export function Edit( { attributes }: { attributes: BlockAttributes } ) {
const blockProps = useBlockProps();
const { name, label, showPricingSection = false } = attributes;
const [ regularPrice, setRegularPrice ] = useEntityProp< string >(
'postType',
'product',
name
);
const context = useContext( CurrencyContext );
const { getCurrencyConfig, formatAmount } = context;
const currencyConfig = getCurrencyConfig();
const inputProps = useCurrencyInputProps( {
value: regularPrice,
setValue: setRegularPrice,
} );
const taxSettingsElement = showPricingSection
? interpolateComponents( {
mixedString: __(
'Manage more settings in {{link}}Pricing.{{/link}}',
'woocommerce'
),
components: {
link: (
<Link
href={ `${ getSetting(
'adminUrl'
) }admin.php?page=wc-settings&tab=tax` }
target="_blank"
type="external"
onClick={ () => {
recordEvent(
'product_pricing_list_price_help_tax_settings_click'
);
} }
>
<></>
</Link>
),
},
} )
: null;
return (
<div { ...blockProps }>
<BaseControl
id={ 'product_pricing_' + name }
help={ taxSettingsElement ? taxSettingsElement : '' }
>
<InputControl
name={ name }
onChange={ setRegularPrice }
label={ label || __( 'Price', 'woocommerce' ) }
value={ formatCurrencyDisplayValue(
String( regularPrice ),
currencyConfig,
formatAmount
) }
{ ...inputProps }
/>
</BaseControl>
</div>
);
}

View File

@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
import { initBlock } from '../../utils';
import metadata from './block.json';
import { Edit } from './edit';
const { name } = metadata;
export { metadata, name };
export const settings = {
example: {},
edit: Edit,
};
export const init = () => initBlock( { name, metadata, settings } );

View File

@ -17,7 +17,7 @@ export function Edit( {
}: {
attributes: BlockAttributes;
context?: {
selectedTab?: string;
selectedTab?: string | null;
};
} ) {
const blockProps = useBlockProps();

View File

@ -0,0 +1,160 @@
/**
* External dependencies
*/
import { render, fireEvent } from '@testing-library/react';
import { getQuery, navigateTo } from '@woocommerce/navigation';
import React, { createElement } from 'react';
import { SlotFillProvider } from '@wordpress/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { Tabs } from '../';
import { Edit as Tab } from '../../tab/edit';
jest.mock( '@wordpress/block-editor', () => ( {
...jest.requireActual( '@wordpress/block-editor' ),
useBlockProps: jest.fn(),
} ) );
jest.mock( '@woocommerce/navigation', () => ( {
...jest.requireActual( '@woocommerce/navigation' ),
navigateTo: jest.fn(),
getQuery: jest.fn().mockReturnValue( {} ),
} ) );
function MockTabs( { onChange = jest.fn() } ) {
const [ selected, setSelected ] = useState< string | null >( null );
const mockContext = {
selectedTab: selected,
};
return (
<SlotFillProvider>
<Tabs
onChange={ ( tabId ) => {
setSelected( tabId );
onChange( tabId );
} }
/>
<Tab
attributes={ { id: 'test1', title: 'Test button 1' } }
context={ mockContext }
/>
<Tab
attributes={ { id: 'test2', title: 'Test button 2' } }
context={ mockContext }
/>
<Tab
attributes={ { id: 'test3', title: 'Test button 3' } }
context={ mockContext }
/>
</SlotFillProvider>
);
}
describe( 'Tabs', () => {
beforeEach( () => {
( getQuery as jest.Mock ).mockReturnValue( {
tab: null,
} );
} );
it( 'should render tab buttons added to the slot', () => {
const { queryByText } = render( <MockTabs /> );
expect( queryByText( 'Test button 1' ) ).toBeInTheDocument();
expect( queryByText( 'Test button 2' ) ).toBeInTheDocument();
} );
it( 'should set the first tab as active initially', () => {
const { queryByText } = render( <MockTabs /> );
expect( queryByText( 'Test button 1' ) ).toHaveAttribute(
'aria-selected',
'true'
);
expect( queryByText( 'Test button 2' ) ).toHaveAttribute(
'aria-selected',
'false'
);
} );
it( 'should navigate to a new URL when a tab is clicked', () => {
const { getByText } = render( <MockTabs /> );
const button = getByText( 'Test button 2' );
fireEvent.click( button );
expect( navigateTo ).toHaveBeenLastCalledWith( {
url: 'admin.php?page=wc-admin&tab=test2',
} );
} );
it( 'should select the tab provided in the URL initially', () => {
( getQuery as jest.Mock ).mockReturnValue( {
tab: 'test2',
} );
const { getByText } = render( <MockTabs /> );
expect( getByText( 'Test button 2' ) ).toHaveAttribute(
'aria-selected',
'true'
);
} );
it( 'should select the tab provided on URL change', () => {
const { getByText, rerender } = render( <MockTabs /> );
( getQuery as jest.Mock ).mockReturnValue( {
tab: 'test3',
} );
rerender( <MockTabs /> );
expect( getByText( 'Test button 3' ) ).toHaveAttribute(
'aria-selected',
'true'
);
} );
it( 'should call the onChange props when changing', async () => {
const mockOnChange = jest.fn();
const { rerender } = render( <MockTabs onChange={ mockOnChange } /> );
expect( mockOnChange ).toHaveBeenCalledWith( 'test1' );
( getQuery as jest.Mock ).mockReturnValue( {
tab: 'test2',
} );
rerender( <MockTabs onChange={ mockOnChange } /> );
expect( mockOnChange ).toHaveBeenCalledWith( 'test2' );
} );
it( 'should add a class to the initially selected tab panel', async () => {
const { getByRole } = render( <MockTabs /> );
const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
expect( panel1.classList ).toContain( 'is-selected' );
expect( panel2.classList ).not.toContain( 'is-selected' );
} );
it( 'should add a class to the newly selected tab panel', async () => {
const { getByText, getByRole, rerender } = render( <MockTabs /> );
const button = getByText( 'Test button 2' );
fireEvent.click( button );
const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
( getQuery as jest.Mock ).mockReturnValue( {
tab: 'test2',
} );
rerender( <MockTabs /> );
expect( panel1.classList ).not.toContain( 'is-selected' );
expect( panel2.classList ).toContain( 'is-selected' );
} );
} );

View File

@ -1,2 +1,3 @@
export { useProductHelper as __experimentalUseProductHelper } from './use-product-helper';
export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order';
export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props';

View File

@ -0,0 +1,77 @@
/**
* External dependencies
*/
import { CurrencyContext } from '@woocommerce/currency';
import { useContext } from '@wordpress/element';
/**
* Internal dependencies
*/
import { useProductHelper } from './use-product-helper';
export type CurrencyInputProps = {
prefix: string;
className: string;
sanitize: ( value: string | number ) => string;
onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void;
onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
};
type Props = {
value: string;
setValue: ( value: string ) => void;
onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
};
export const useCurrencyInputProps = ( {
value,
setValue,
onFocus,
onKeyUp,
}: Props ) => {
const { sanitizePrice } = useProductHelper();
const context = useContext( CurrencyContext );
const { getCurrencyConfig } = context;
const currencyConfig = getCurrencyConfig();
const currencyInputProps: CurrencyInputProps = {
prefix: currencyConfig.symbol,
className: 'half-width-field components-currency-control',
sanitize: ( val: string | number ) => {
return sanitizePrice( String( val ) );
},
onFocus( event: React.FocusEvent< HTMLInputElement > ) {
// In some browsers like safari .select() function inside
// the onFocus event doesn't work as expected because it
// conflicts with onClick the first time user click the
// input. Using setTimeout defers the text selection and
// avoid the unexpected behaviour.
setTimeout(
function deferSelection( element: HTMLInputElement ) {
element.select();
},
0,
event.currentTarget
);
if ( onFocus ) {
onFocus( event );
}
},
onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) {
const amount = Number.parseFloat( sanitizePrice( value || '0' ) );
const step = Number( event.currentTarget.step || '1' );
if ( event.code === 'ArrowUp' ) {
setValue( String( amount + step ) );
}
if ( event.code === 'ArrowDown' ) {
setValue( String( amount - step ) );
}
if ( onKeyUp ) {
onKeyUp( event );
}
},
};
return currencyInputProps;
};

View File

@ -7,3 +7,4 @@ export const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE =
export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized';
export const PRODUCT_VARIATION_TITLE_LIMIT = 32;
export const STANDARD_RATE_TAX_CLASS_SLUG = 'standard';
export const AUTO_DRAFT_NAME = 'AUTO-DRAFT';

View File

@ -0,0 +1,35 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { AUTO_DRAFT_NAME } from './constants';
/**
* Get the header title using the product name.
*
* @param editedProductName Name value entered for the product.
* @param initialProductName Name already persisted to the database.
* @return The new title
*/
export const getHeaderTitle = (
editedProductName: string,
initialProductName: string
): string => {
const isProductNameNotEmpty = Boolean( editedProductName );
const isProductNameDirty = editedProductName !== initialProductName;
const isCreating = initialProductName === AUTO_DRAFT_NAME;
if ( isProductNameNotEmpty && isProductNameDirty ) {
return editedProductName;
}
if ( isCreating ) {
return __( 'Add new product', 'woocommerce' );
}
return initialProductName;
};

View File

@ -51,7 +51,9 @@ export const getProductStockStatus = (
}
if ( product.stock_status ) {
return PRODUCT_STOCK_STATUS_LABELS[ product.stock_status ];
return PRODUCT_STOCK_STATUS_LABELS[
product.stock_status as PRODUCT_STOCK_STATUS_KEYS
];
}
return PRODUCT_STOCK_STATUS_LABELS.instock;
@ -77,6 +79,8 @@ export const getProductStockStatusClass = (
return PRODUCT_STOCK_STATUS_CLASSES.outofstock;
}
return product.stock_status
? PRODUCT_STOCK_STATUS_CLASSES[ product.stock_status ]
? PRODUCT_STOCK_STATUS_CLASSES[
product.stock_status as PRODUCT_STOCK_STATUS_KEYS
]
: '';
};

View File

@ -3,7 +3,10 @@
*/
import { __ } from '@wordpress/i18n';
export const AUTO_DRAFT_NAME = 'AUTO-DRAFT';
/**
* Internal dependencies
*/
import { AUTO_DRAFT_NAME } from './constants';
/**
* Get the product title for use in the header.

View File

@ -1,16 +1,18 @@
/**
* Internal dependencies
*/
import { AUTO_DRAFT_NAME } from './constants';
import { formatCurrencyDisplayValue } from './format-currency-display-value';
import { getCheckboxTracks } from './get-checkbox-tracks';
import { getCurrencySymbolProps } from './get-currency-symbol-props';
import { getDerivedProductType } from './get-derived-product-type';
import { getHeaderTitle } from './get-header-title';
import { getProductStatus, PRODUCT_STATUS_LABELS } from './get-product-status';
import {
getProductStockStatus,
getProductStockStatusClass,
} from './get-product-stock-status';
import { getProductTitle, AUTO_DRAFT_NAME } from './get-product-title';
import { getProductTitle } from './get-product-title';
import {
getProductVariationTitle,
getTruncatedProductVariationTitle,
@ -27,6 +29,7 @@ export {
getCheckboxTracks,
getCurrencySymbolProps,
getDerivedProductType,
getHeaderTitle,
getProductStatus,
getProductStockStatus,
getProductStockStatusClass,

View File

@ -6,4 +6,3 @@ declare global {
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
export {};

View File

@ -0,0 +1,18 @@
declare module '@woocommerce/settings' {
export declare function getAdminLink( path: string ): string;
export declare function getSetting< T >(
name: string,
fallback?: unknown,
filter = ( val: unknown, fb: unknown ) =>
typeof val !== 'undefined' ? val : fb
): T;
}
declare module '@wordpress/core-data' {
function useEntityProp< T = unknown >(
kind: string,
name: string,
prop: string,
id?: string
): [ T, ( value: T ) => void, T ];
}

View File

@ -64,7 +64,7 @@ const CollapsibleCard: React.FC< CollapsibleCardProps > = ( {
{ ! collapsed && (
<>
{ children }
{ footer && <CardFooter>{ footer }</CardFooter> }
{ !! footer && <CardFooter>{ footer }</CardFooter> }
</>
) }
</Card>

View File

@ -105,7 +105,7 @@ export const CreateNewCampaignModal = ( props: CreateCampaignModalProps ) => {
<FlexItem>
{ __( 'Create', 'woocommerce' ) }
</FlexItem>
{ isExternalURL( el.createUrl ) && (
{ !! isExternalURL( el.createUrl ) && (
<FlexItem>
<Icon
icon={ external }

View File

@ -79,7 +79,7 @@ const KnowledgeBase = ( {
target="_blank"
rel="noopener noreferrer"
>
{ post.image && (
{ !! post.image && (
<div className="woocommerce-marketing-knowledgebase-card__post-img">
<img src={ post.image } alt="" />
</div>
@ -89,7 +89,7 @@ const KnowledgeBase = ( {
<p className="woocommerce-marketing-knowledgebase-card__post-meta">
{ __( 'By', 'woocommerce' ) + ' ' }
{ post.author_name }
{ post.author_avatar && (
{ !! post.author_avatar && (
<img
src={ post.author_avatar.replace(
's=96',

View File

@ -22,7 +22,7 @@ const CouponsOverview = () => {
return (
<div className="woocommerce-marketing-coupons">
{ shouldShowExtensions && (
{ !! shouldShowExtensions && (
<RecommendedExtensions
title={ __(
'Recommended coupon extensions',

View File

@ -141,7 +141,7 @@ export const Campaigns = () => {
{ el.title }
</Link>
</FlexItem>
{ el.description && (
{ !! el.description && (
<FlexItem className="woocommerce-marketing-campaigns-card__campaign-description">
{ el.description }
</FlexItem>
@ -170,14 +170,14 @@ export const Campaigns = () => {
>
{ __( 'Create new campaign', 'woocommerce' ) }
</Button>
{ isModalOpen && (
{ !! isModalOpen && (
<CreateNewCampaignModal
onRequestClose={ () => setModalOpen( false ) }
/>
) }
</CardHeader>
{ getContent() }
{ total && total > perPage && (
{ !! ( total && total > perPage ) && (
<CardFooter className="woocommerce-marketing-campaigns-card__footer">
<Pagination
showPerPagePicker={ false }

View File

@ -78,7 +78,7 @@ export const Channels: React.FC< ChannelsProps > = ( {
{ /* Recommended channels section. */ }
{ recommendedChannels.length >= 1 && (
<div>
{ hasRegisteredChannels && (
{ !! hasRegisteredChannels && (
<>
<CardDivider />
<CardBody>
@ -98,7 +98,7 @@ export const Channels: React.FC< ChannelsProps > = ( {
</CardBody>
</>
) }
{ expanded &&
{ !! expanded &&
recommendedChannels.map( ( el, idx ) => {
return (
<Fragment key={ el.plugin }>

View File

@ -31,7 +31,7 @@ export const RegisteredChannelCardBody: React.FC<
registeredChannel.description
) : (
<div className="woocommerce-marketing-registered-channel-description">
{ registeredChannel.syncStatus && (
{ !! registeredChannel.syncStatus && (
<>
<SyncStatus status={ registeredChannel.syncStatus } />
<div className="woocommerce-marketing-registered-channel-description__separator" />

View File

@ -27,7 +27,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => {
} }
>
<div className="woocommerce-marketing-learn-marketing-card__post-img">
{ post.image && <img src={ post.image } alt="" /> }
{ !! post.image && <img src={ post.image } alt="" /> }
</div>
<div className="woocommerce-marketing-learn-marketing-card__post-title">
{ post.title }
@ -37,7 +37,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => {
// translators: %s: author's name.
sprintf( __( 'By %s', 'woocommerce' ), post.author_name )
}
{ post.author_avatar && (
{ !! post.author_avatar && (
<img
src={ post.author_avatar.replace( 's=96', 's=32' ) }
alt=""

View File

@ -77,8 +77,7 @@ export const MarketingOverviewMultichannel: React.FC = () => {
/>
) }
{ !! dataRegistered?.length && <Campaigns /> }
{ dataRegistered &&
dataRecommended &&
{ !! ( dataRegistered && dataRecommended ) &&
!! ( dataRegistered.length || dataRecommended.length ) && (
<Channels
addChannelsButtonRef={ addChannelsButtonRef }
@ -88,7 +87,7 @@ export const MarketingOverviewMultichannel: React.FC = () => {
/>
) }
<InstalledExtensions />
{ shouldShowExtensions && <DiscoverTools /> }
{ !! shouldShowExtensions && <DiscoverTools /> }
<LearnMarketing />
</div>
);

View File

@ -27,7 +27,7 @@ const MarketingOverview = () => {
<WelcomeCard />
<InstalledExtensions />
<MarketingOverviewSectionSlot />
{ shouldShowExtensions && (
{ !! shouldShowExtensions && (
<RecommendedExtensions category="marketing" />
) }
<KnowledgeBase category="marketing" />

View File

@ -193,7 +193,14 @@ const PaymentRecommendations: React.FC = () => {
</Button>
),
before: (
<img src={ plugin.square_image || plugin.image } alt="" />
<img
src={
plugin.square_image ||
plugin.image_72x72 ||
plugin.image
}
alt=""
/>
),
};
} );

View File

@ -0,0 +1,47 @@
.woocommerce-product-block-editor {
.components-input-control {
&__prefix {
margin-left: $gap-smaller;
}
&__suffix {
margin-right: $gap-smaller;
}
}
.components-currency-control {
.components-input-control__prefix {
color: $gray-700;
}
.components-input-control__input {
text-align: right;
}
}
.woocommerce-product-form {
&__custom-label-input {
display: flex;
flex-direction: column;
label {
display: block;
margin-bottom: $gap-smaller;
}
}
&__optional-input {
color: $gray-700;
}
}
.wp-block-columns {
gap: $gap-large;
}
.wp-block-woocommerce-product-section {
> .block-editor-inner-blocks > .block-editor-block-list__layout > .wp-block:not(:first-child) {
margin-top: $gap-large;
}
}
}

View File

@ -16,6 +16,7 @@ import { useParams } from 'react-router-dom';
* Internal dependencies
*/
import './product-page.scss';
import './product-block-page.scss';
declare const productBlockEditorSettings: ProductEditorSettings;
@ -34,18 +35,23 @@ const ProductEditor: React.FC< { product: Product | undefined } > = ( {
);
};
const EditProductEditor: React.FC< { productId: string } > = ( {
const EditProductEditor: React.FC< { productId: number } > = ( {
productId,
} ) => {
const { product } = useSelect( ( select: typeof WPSelect ) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Missing types.
const { getEditedEntityRecord } = select( 'core' );
const { product } = useSelect(
( select: typeof WPSelect ) => {
const { getEntityRecord } = select( 'core' );
return {
product: getEditedEntityRecord( 'postType', 'product', productId ),
product: getEntityRecord(
'postType',
'product',
productId
) as Product,
};
} );
},
[ productId ]
);
return <ProductEditor product={ product } />;
};
@ -74,7 +80,9 @@ export default function ProductPage() {
const { productId } = useParams();
if ( productId ) {
return <EditProductEditor productId={ productId } />;
return (
<EditProductEditor productId={ Number.parseInt( productId, 10 ) } />
);
}
return <AddProductEditor />;

View File

@ -31,6 +31,7 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => {
settingsUrl: manageUrl,
is_local_partner: isLocalPartner,
external_link: externalLink,
transaction_processors: transactionProcessors,
} = paymentGateway;
const connectSlot = useSlot(
@ -88,6 +89,21 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => {
<div className="woocommerce-task-payment__content">
{ content }
</div>
{ transactionProcessors && (
<div className="woocommerce-task-payment__transaction-processors_images">
{ Object.keys( transactionProcessors ).map(
( key ) => {
return (
<img
src={ transactionProcessors[ key ] }
alt={ key }
key={ key }
/>
);
}
) }
</div>
) }
</div>
<div className="woocommerce-task-payment__footer">
<Action

View File

@ -67,6 +67,18 @@
font-size: 12px;
}
}
.woocommerce-task-payment__transaction-processors_images {
padding-top: 16px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
img {
height: 24px;
}
}
.woocommerce-task-payment__description {
flex: 1;
}

View File

@ -8,7 +8,9 @@
grid-template-columns: auto auto;
grid-column-gap: 16px;
grid-row-gap: 24px;
justify-content: start;
// Set it to center here since we only have one import option.
// We can change it to start when we have more import options.
justify-content: center;
}
.woocommerce-list__item {

View File

@ -3,10 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import PageIcon from 'gridicons/dist/pages';
import ReblogIcon from 'gridicons/dist/reblog';
import { getAdminLink } from '@woocommerce/settings';
import interpolateComponents from '@automattic/interpolate-components';
import { ExternalLink } from '@wordpress/components';
import { recordEvent } from '@woocommerce/tracks';
export const importTypes = [
@ -25,32 +22,4 @@ export const importTypes = [
);
},
},
{
key: 'from-cart2cart' as const,
title: __( 'FROM CART2CART', 'woocommerce' ),
content: interpolateComponents( {
mixedString: __(
'Migrate all store data like products, customers, and orders in no time with this 3rd party plugin. {{link}}Learn more{{/link}}',
'woocommerce'
),
components: {
link: (
<ExternalLink
href="https://woocommerce.com/products/cart2cart/?utm_medium=product"
onClickCapture={ ( e ) => e.preventDefault() }
></ExternalLink>
),
},
} ),
before: <ReblogIcon />,
onClick: () => {
recordEvent( 'tasklist_add_product', { method: 'migrate' } );
window
.open(
'https://woocommerce.com/products/cart2cart/?utm_medium=product',
'_blank'
)
?.focus();
},
},
];

View File

@ -57,24 +57,6 @@ describe( 'Products', () => {
);
} );
test( 'should fire "tasklist_add_product" event when the cart2cart option clicked', async () => {
const { getByRole } = render( <Products /> );
userEvent.click(
getByRole( 'menuitem', {
name: 'FROM CART2CART Migrate all store data like products, customers, and orders in no time with this 3rd party plugin. Learn more (opens in a new tab)',
} )
);
await waitFor( () =>
expect( recordEvent ).toHaveBeenCalledWith(
'tasklist_add_product',
{
method: 'migrate',
}
)
);
} );
test( 'should fire "task_completion_time" event when an option clicked', async () => {
Object.defineProperty( window, 'performance', {
value: {

View File

@ -4,7 +4,6 @@
import { __ } from '@wordpress/i18n';
import interpolateComponents from '@automattic/interpolate-components';
import { Text } from '@woocommerce/experimental';
import { ExternalLink } from '@wordpress/components';
import { Link } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
@ -25,7 +24,7 @@ const Footer: React.FC = () => {
<Text className="woocommerce-products-footer__import-options">
{ interpolateComponents( {
mixedString: __(
'{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}} or {{_3rdLink}}use a 3rd party migration plugin{{/_3rdLink}}.',
'{{importCSVLink}}Import your products from a CSV file{{/importCSVLink}}.',
'woocommerce'
),
components: {
@ -47,20 +46,6 @@ const Footer: React.FC = () => {
<></>
</Link>
),
_3rdLink: (
<ExternalLink
onClick={ () => {
recordEvent( 'tasklist_add_product', {
method: 'migrate',
} );
recordCompletionTime();
} }
href="https://woocommerce.com/products/cart2cart/?utm_medium=product"
type="external"
>
<></>
</ExternalLink>
),
},
} ) }
</Text>

View File

@ -16,9 +16,9 @@ describe( 'Footer', () => {
beforeEach( () => {
( recordEvent as jest.Mock ).mockClear();
} );
it( 'should render footer with two links', () => {
it( 'should render footer with one links', () => {
const { queryAllByRole } = render( <Footer /> );
expect( queryAllByRole( 'link' ) ).toHaveLength( 2 );
expect( queryAllByRole( 'link' ) ).toHaveLength( 1 );
} );
it( 'clicking on import CSV should fire event tasklist_add_product with method:import and task_completion_time', () => {
@ -35,19 +35,4 @@ describe( 'Footer', () => {
{ task_name: 'products', time: '0-2s' }
);
} );
it( 'clicking on start blank should fire event tasklist_add_product with method:migrate and task_completion_time', () => {
const { getByText } = render( <Footer /> );
userEvent.click( getByText( 'use a 3rd party migration plugin' ) );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'migrate' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
} );

View File

@ -33,12 +33,12 @@
"lint:js-pre-commit": "eslint --ext=js,ts,tsx",
"prepack": "pnpm install && pnpm run lint && pnpm run test && cross-env WC_ADMIN_PHASE=core pnpm run build",
"packages:fix:textdomain": "node ./bin/package-update-textdomain.js",
"packages:build": "cross-env WC_ADMIN_PHASE=development pnpm -w run build --filter='./packages/js/*'",
"packages:watch": "cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel",
"run:packages": "pnpm run --filter ../../packages/js/",
"packages:build": "pnpm run:packages -- build",
"packages:watch": "pnpm run:packages -- start",
"run:packages": "pnpm run --parallel --filter='../../packages/js/**'",
"prestart": "pnpm packages:build && cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config",
"start": "concurrently \"cross-env WC_ADMIN_PHASE=development webpack --watch\" \"cross-env WC_ADMIN_PHASE=development pnpm packages:watch\"",
"start:hot": "pnpm prestart && concurrently \"cross-env WC_ADMIN_PHASE=development webpack serve\" \"cross-env WC_ADMIN_PHASE=development pnpm packages:watch\"",
"start:hot": "pnpm prestart && concurrently \"cross-env HOT=true WC_ADMIN_PHASE=development webpack serve\" \"cross-env WC_ADMIN_PHASE=development pnpm packages:watch\"",
"test-staged": "pnpm run test:client --bail --findRelatedTests",
"test:client": "jest --config client/jest.config.js",
"test:debug": "node --inspect-brk ./node_modules/.bin/jest --config client/jest.config.js --watch --runInBand --no-cache",

View File

@ -22,6 +22,7 @@ const WooCommerceDependencyExtractionWebpackPlugin = require( '../../packages/js
const NODE_ENV = process.env.NODE_ENV || 'development';
const WC_ADMIN_PHASE = process.env.WC_ADMIN_PHASE || 'development';
const isHot = Boolean( process.env.HOT );
const isProduction = NODE_ENV === 'production';
const wcAdminPackages = [
@ -134,6 +135,7 @@ const webpackConfig = {
plugins: [
'@babel/plugin-proposal-class-properties',
! isProduction &&
isHot &&
require.resolve( 'react-refresh/babel' ),
].filter( Boolean ),
},
@ -190,7 +192,7 @@ const webpackConfig = {
} ) ),
} ),
// React Fast Refresh.
! isProduction && new ReactRefreshWebpackPlugin(),
! isProduction && isHot && new ReactRefreshWebpackPlugin(),
// We reuse this Webpack setup for Storybook, where we need to disable dependency extraction.
! process.env.STORYBOOK &&
@ -228,6 +230,8 @@ const webpackConfig = {
if ( ! isProduction || WC_ADMIN_PHASE === 'development' ) {
// Set default sourcemap mode if it wasn't set by WP_DEVTOOL.
webpackConfig.devtool = webpackConfig.devtool || 'source-map';
if ( isHot ) {
// Add dev server config
// Copied from https://github.com/WordPress/gutenberg/blob/05bea6dd5c6198b0287c41a401d36a06b48831eb/packages/scripts/config/webpack.config.js#L312-L326
webpackConfig.devServer = {
@ -246,5 +250,6 @@ if ( ! isProduction || WC_ADMIN_PHASE === 'development' ) {
},
};
}
}
module.exports = webpackConfig;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Add productId dependency when getting the product by id in ProductPage

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update product template by adding the list price and sale price blocks.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Added images support for the payment recommendations transaction processors

View File

@ -0,0 +1,5 @@
Significance: patch
Type: dev
Comment: Use React Fast Refresh for WooCommerce Admin "start:hot" command only; fix empty page issue with "start" command when SCRIPT_DEBUG is set to false.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix React rendering falsy value in marketing page.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix no enforcing of min/max limits in quantity selector of variable products.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Load same stylesheets in the Site Editor as in the frontend

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fixed payments recommendations pane in WooCommerce Payment Settings using the wrong image prop

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Record values for toggled checkboxes/features in settings

View File

@ -0,0 +1,5 @@
Significance: patch
Type: dev
Comment: Fix woocommerce-admin run packages commands

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fixes filtering by attributes in the Analytics Orders and Variations reports.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: performance
Bypass Action Scheduler for customer updates.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update obw payment gateways

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove Cart2Cart option from add product task

View File

@ -365,63 +365,6 @@
}
// Description/Additional info/Reviews tabs.
.woocommerce-tabs {
padding-top: var(--wp--style--block-gap);
ul.wc-tabs {
padding: 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #eae9eb;
li {
opacity: 0.5;
color: var(--wp--preset--color--contrast);
margin: 0;
padding: 0.5em 1em 0.5em 1em;
border-color: #eae9eb;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
float: left;
border-style: solid;
border-width: 1px;
font-weight: 600;
font-size: var(--wp--preset--font-size--medium);
&:first-child {
border-left-color: #eae9eb;
margin-left: 1em;
}
&.active {
background: var(--wp--preset--color--tertiary);
color: var(--wp--preset--color--contrast);
opacity: 1;
}
a {
text-decoration: none;
color: var(--wp--preset--color--contrast);
}
}
}
.woocommerce-Tabs-panel {
padding-top: var(--wp--style--block-gap);
font-size: var(--wp--preset--font-size--small);
margin-left: 1em;
h2 {
display: none;
}
table.woocommerce-product-attributes {
text-align: left;
}
}
}
// Reviews tab.
.woocommerce-Reviews {
ol.commentlist {
@ -593,6 +536,63 @@
}
// Description/Additional info/Reviews tabs.
.woocommerce-tabs {
padding-top: var(--wp--style--block-gap);
}
ul.wc-tabs {
padding: 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #eae9eb;
li {
opacity: 0.5;
color: var(--wp--preset--color--contrast);
margin: 0;
padding: 0.5em 1em 0.5em 1em;
border-color: #eae9eb;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
float: left;
border-style: solid;
border-width: 1px;
font-weight: 600;
font-size: var(--wp--preset--font-size--medium);
&:first-child {
border-left-color: #eae9eb;
margin-left: 1em;
}
&.active {
background: var(--wp--preset--color--tertiary);
color: var(--wp--preset--color--contrast);
opacity: 1;
}
a {
text-decoration: none;
color: var(--wp--preset--color--contrast);
}
}
}
.woocommerce-Tabs-panel {
padding-top: var(--wp--style--block-gap);
font-size: var(--wp--preset--font-size--small);
margin-left: 1em;
h2 {
display: none;
}
table.woocommerce-product-attributes {
text-align: left;
}
}
.woocommerce-page {
.woocommerce-cart-form {

View File

@ -369,63 +369,6 @@ $tt2-gray: #f7f7f7;
}
}
.woocommerce-tabs {
padding-top: var(--wp--style--block-gap);
ul.wc-tabs {
padding: 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #eae9eb;
li {
background: #eae9eb;
margin: 0;
padding: 0.5em 1em 0.5em 1em;
border-color: #eae9eb;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
float: left;
border-style: solid;
border-width: 1px;
border-left-color: var(--wp--preset--color--background);
font-weight: 600;
font-size: var(--wp--preset--font-size--medium);
&:first-child {
border-left-color: #eae9eb;
margin-left: 1em;
}
&.active {
box-shadow: 0 1px var(--wp--preset--color--background);
}
a {
text-decoration: none;
}
}
}
// Moved from blocktheme.scss to retain full styling.
ul.tabs li.active {
// Style active tab in theme colors.
background: var(--wp--preset--color--background, $contentbg);
border-bottom-color: var(--wp--preset--color--background, $contentbg);
}
.woocommerce-Tabs-panel {
padding-top: var(--wp--style--block-gap);
font-size: var(--wp--preset--font-size--small);
margin-left: 1em;
h2 {
display: none;
}
}
}
.woocommerce-Reviews {
ol.commentlist {
list-style: none;
@ -603,6 +546,59 @@ $tt2-gray: #f7f7f7;
}
}
// Description/Additional info/Reviews tabs.
.woocommerce-tabs {
padding-top: var(--wp--style--block-gap);
}
ul.wc-tabs {
padding: 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-bottom-color: #eae9eb;
li {
background: #eae9eb;
margin: 0;
padding: 0.5em 1em 0.5em 1em;
border-color: #eae9eb;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
float: left;
border-style: solid;
border-width: 1px;
border-left-color: var(--wp--preset--color--background);
font-weight: 600;
font-size: var(--wp--preset--font-size--medium);
&:first-child {
border-left-color: #eae9eb;
margin-left: 1em;
}
&.active {
// Style active tab in theme colors.
background: var(--wp--preset--color--background, $contentbg);
border-bottom-color: var(--wp--preset--color--background, $contentbg);
box-shadow: 0 1px var(--wp--preset--color--background);
}
a {
text-decoration: none;
}
}
}
.woocommerce-Tabs-panel {
padding-top: var(--wp--style--block-gap);
font-size: var(--wp--preset--font-size--small);
margin-left: 1em;
h2 {
display: none;
}
}
/**
* Form fields
*/

View File

@ -56,12 +56,22 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
wp_style_add_data( 'woocommerce_admin_privacy_styles', 'rtl', 'replace' );
if ( $screen && $screen->is_block_editor() ) {
wp_register_style( 'woocommerce-general', WC()->plugin_url() . '/assets/css/woocommerce.css', array(), $version );
wp_style_add_data( 'woocommerce-general', 'rtl', 'replace' );
if ( wc_current_theme_is_fse_theme() ) {
wp_register_style( 'woocommerce-blocktheme', WC()->plugin_url() . '/assets/css/woocommerce-blocktheme.css', array(), $version );
wp_style_add_data( 'woocommerce-blocktheme', 'rtl', 'replace' );
wp_enqueue_style( 'woocommerce-blocktheme' );
$styles = WC_Frontend_Scripts::get_styles();
if ( $styles ) {
foreach ( $styles as $handle => $args ) {
wp_register_style(
$handle,
$args['src'],
$args['deps'],
$args['version'],
$args['media']
);
if ( ! isset( $args['has_rtl'] ) ) {
wp_style_add_data( $handle, 'rtl', 'replace' );
}
}
}
}

View File

@ -1001,6 +1001,8 @@ class WC_Install {
* woocommerce_order_itemmeta - Order line item meta is stored in a table for storing extra data.
* woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient.
* woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table.
*
* @return array Strings containing the results of the various update queries as returned by dbDelta.
*/
public static function create_tables() {
global $wpdb;
@ -1015,7 +1017,7 @@ class WC_Install {
*/
if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_downloadable_product_permissions';" ) ) {
if ( ! $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_downloadable_product_permissions` LIKE 'permission_id';" ) ) {
$wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT;" );
$wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` bigint(20) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT;" );
}
}
@ -1035,7 +1037,7 @@ class WC_Install {
}
}
dbDelta( self::get_schema() );
$db_delta_result = dbDelta( self::get_schema() );
$index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" );
@ -1047,6 +1049,8 @@ class WC_Install {
// Clear table caches.
delete_transient( 'wc_attribute_taxonomies' );
return $db_delta_result;
}
/**
@ -1085,16 +1089,16 @@ class WC_Install {
$tables = "
CREATE TABLE {$wpdb->prefix}woocommerce_sessions (
session_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
session_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
session_key char(32) NOT NULL,
session_value longtext NOT NULL,
session_expiry BIGINT UNSIGNED NOT NULL,
session_expiry bigint(20) unsigned NOT NULL,
PRIMARY KEY (session_id),
UNIQUE KEY session_key (session_key)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_api_keys (
key_id BIGINT UNSIGNED NOT NULL auto_increment,
user_id BIGINT UNSIGNED NOT NULL,
key_id bigint(20) unsigned NOT NULL auto_increment,
user_id bigint(20) unsigned NOT NULL,
description varchar(200) NULL,
permissions varchar(10) NOT NULL,
consumer_key char(64) NOT NULL,
@ -1107,7 +1111,7 @@ CREATE TABLE {$wpdb->prefix}woocommerce_api_keys (
KEY consumer_secret (consumer_secret)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies (
attribute_id BIGINT UNSIGNED NOT NULL auto_increment,
attribute_id bigint(20) unsigned NOT NULL auto_increment,
attribute_name varchar(200) NOT NULL,
attribute_label varchar(200) NULL,
attribute_type varchar(20) NOT NULL,
@ -1117,17 +1121,17 @@ CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies (
KEY attribute_name (attribute_name(20))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions (
permission_id BIGINT UNSIGNED NOT NULL auto_increment,
permission_id bigint(20) unsigned NOT NULL auto_increment,
download_id varchar(36) NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
order_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
product_id bigint(20) unsigned NOT NULL,
order_id bigint(20) unsigned NOT NULL DEFAULT 0,
order_key varchar(200) NOT NULL,
user_email varchar(200) NOT NULL,
user_id BIGINT UNSIGNED NULL,
user_id bigint(20) unsigned NULL,
downloads_remaining varchar(9) NULL,
access_granted datetime NOT NULL default '0000-00-00 00:00:00',
access_expires datetime NULL default null,
download_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
download_count bigint(20) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (permission_id),
KEY download_order_key_product (product_id,order_id,order_key(16),download_id),
KEY download_order_product (download_id,order_id,product_id),
@ -1135,16 +1139,16 @@ CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions (
KEY user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_order_items (
order_item_id BIGINT UNSIGNED NOT NULL auto_increment,
order_item_name TEXT NOT NULL,
order_item_id bigint(20) unsigned NOT NULL auto_increment,
order_item_name text NOT NULL,
order_item_type varchar(200) NOT NULL DEFAULT '',
order_id BIGINT UNSIGNED NOT NULL,
order_id bigint(20) unsigned NOT NULL,
PRIMARY KEY (order_item_id),
KEY order_id (order_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta (
meta_id BIGINT UNSIGNED NOT NULL auto_increment,
order_item_id BIGINT UNSIGNED NOT NULL,
meta_id bigint(20) unsigned NOT NULL auto_increment,
order_item_id bigint(20) unsigned NOT NULL,
meta_key varchar(255) default NULL,
meta_value longtext NULL,
PRIMARY KEY (meta_id),
@ -1152,15 +1156,15 @@ CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta (
KEY meta_key (meta_key(32))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates (
tax_rate_id BIGINT UNSIGNED NOT NULL auto_increment,
tax_rate_id bigint(20) unsigned NOT NULL auto_increment,
tax_rate_country varchar(2) NOT NULL DEFAULT '',
tax_rate_state varchar(200) NOT NULL DEFAULT '',
tax_rate varchar(8) NOT NULL DEFAULT '',
tax_rate_name varchar(200) NOT NULL DEFAULT '',
tax_rate_priority BIGINT UNSIGNED NOT NULL,
tax_rate_priority bigint(20) unsigned NOT NULL,
tax_rate_compound int(1) NOT NULL DEFAULT 0,
tax_rate_shipping int(1) NOT NULL DEFAULT 1,
tax_rate_order BIGINT UNSIGNED NOT NULL,
tax_rate_order bigint(20) unsigned NOT NULL,
tax_rate_class varchar(200) NOT NULL DEFAULT '',
PRIMARY KEY (tax_rate_id),
KEY tax_rate_country (tax_rate_country),
@ -1169,23 +1173,23 @@ CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates (
KEY tax_rate_priority (tax_rate_priority)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations (
location_id BIGINT UNSIGNED NOT NULL auto_increment,
location_id bigint(20) unsigned NOT NULL auto_increment,
location_code varchar(200) NOT NULL,
tax_rate_id BIGINT UNSIGNED NOT NULL,
tax_rate_id bigint(20) unsigned NOT NULL,
location_type varchar(40) NOT NULL,
PRIMARY KEY (location_id),
KEY tax_rate_id (tax_rate_id),
KEY location_type_code (location_type(10),location_code(20))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zones (
zone_id BIGINT UNSIGNED NOT NULL auto_increment,
zone_id bigint(20) unsigned NOT NULL auto_increment,
zone_name varchar(200) NOT NULL,
zone_order BIGINT UNSIGNED NOT NULL,
zone_order bigint(20) unsigned NOT NULL,
PRIMARY KEY (zone_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations (
location_id BIGINT UNSIGNED NOT NULL auto_increment,
zone_id BIGINT UNSIGNED NOT NULL,
location_id bigint(20) unsigned NOT NULL auto_increment,
zone_id bigint(20) unsigned NOT NULL,
location_code varchar(200) NOT NULL,
location_type varchar(40) NOT NULL,
PRIMARY KEY (location_id),
@ -1193,26 +1197,26 @@ CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations (
KEY location_type_code (location_type(10),location_code(20))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods (
zone_id BIGINT UNSIGNED NOT NULL,
instance_id BIGINT UNSIGNED NOT NULL auto_increment,
zone_id bigint(20) unsigned NOT NULL,
instance_id bigint(20) unsigned NOT NULL auto_increment,
method_id varchar(200) NOT NULL,
method_order BIGINT UNSIGNED NOT NULL,
method_order bigint(20) unsigned NOT NULL,
is_enabled tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (instance_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens (
token_id BIGINT UNSIGNED NOT NULL auto_increment,
token_id bigint(20) unsigned NOT NULL auto_increment,
gateway_id varchar(200) NOT NULL,
token text NOT NULL,
user_id BIGINT UNSIGNED NOT NULL DEFAULT '0',
user_id bigint(20) unsigned NOT NULL DEFAULT '0',
type varchar(200) NOT NULL,
is_default tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (token_id),
KEY user_id (user_id)
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta (
meta_id BIGINT UNSIGNED NOT NULL auto_increment,
payment_token_id BIGINT UNSIGNED NOT NULL,
meta_id bigint(20) unsigned NOT NULL auto_increment,
payment_token_id bigint(20) unsigned NOT NULL,
meta_key varchar(255) NULL,
meta_value longtext NULL,
PRIMARY KEY (meta_id),
@ -1220,7 +1224,7 @@ CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta (
KEY meta_key (meta_key(32))
) $collate;
CREATE TABLE {$wpdb->prefix}woocommerce_log (
log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
log_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
timestamp datetime NOT NULL,
level smallint(4) NOT NULL,
source varchar(200) NOT NULL,
@ -1230,10 +1234,10 @@ CREATE TABLE {$wpdb->prefix}woocommerce_log (
KEY level (level)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_webhooks (
webhook_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
webhook_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
status varchar(200) NOT NULL,
name text NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
user_id bigint(20) unsigned NOT NULL,
delivery_url text NOT NULL,
secret text NOT NULL,
topic varchar(200) NOT NULL,
@ -1248,11 +1252,11 @@ CREATE TABLE {$wpdb->prefix}wc_webhooks (
KEY user_id (user_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_download_log (
download_log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
download_log_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
timestamp datetime NOT NULL,
permission_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NULL,
user_ip_address VARCHAR(100) NULL DEFAULT '',
permission_id bigint(20) unsigned NOT NULL,
user_id bigint(20) unsigned NULL,
user_ip_address varchar(100) NULL DEFAULT '',
PRIMARY KEY (download_log_id),
KEY permission_id (permission_id),
KEY timestamp (timestamp)
@ -1281,7 +1285,7 @@ CREATE TABLE {$wpdb->prefix}wc_product_meta_lookup (
KEY min_max_price (`min_price`, `max_price`)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
tax_rate_class_id BIGINT UNSIGNED NOT NULL auto_increment,
tax_rate_class_id bigint(20) unsigned NOT NULL auto_increment,
name varchar(200) NOT NULL DEFAULT '',
slug varchar(200) NOT NULL DEFAULT '',
PRIMARY KEY (tax_rate_class_id),
@ -1296,18 +1300,18 @@ CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
PRIMARY KEY (`order_id`, `product_id`)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_rate_limits (
rate_limit_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
rate_limit_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
rate_limit_key varchar(200) NOT NULL,
rate_limit_expiry BIGINT UNSIGNED NOT NULL,
rate_limit_expiry bigint(20) unsigned NOT NULL,
rate_limit_remaining smallint(10) NOT NULL DEFAULT '0',
PRIMARY KEY (rate_limit_id),
UNIQUE KEY rate_limit_key (rate_limit_key($max_index_length))
) $collate;
$product_attributes_lookup_table_creation_sql
CREATE TABLE {$wpdb->prefix}wc_product_download_directories (
url_id BIGINT UNSIGNED NOT NULL auto_increment,
url_id bigint(20) unsigned NOT NULL auto_increment,
url varchar(256) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 0,
enabled tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (url_id),
KEY url (url($max_index_length))
) $collate;
@ -1323,22 +1327,22 @@ CREATE TABLE {$wpdb->prefix}wc_order_stats (
tax_total double DEFAULT 0 NOT NULL,
shipping_total double DEFAULT 0 NOT NULL,
net_total double DEFAULT 0 NOT NULL,
returning_customer boolean DEFAULT NULL,
returning_customer tinyint(1) DEFAULT NULL,
status varchar(200) NOT NULL,
customer_id BIGINT UNSIGNED NOT NULL,
customer_id bigint(20) unsigned NOT NULL,
PRIMARY KEY (order_id),
KEY date_created (date_created),
KEY customer_id (customer_id),
KEY status (status({$max_index_length}))
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_product_lookup (
order_item_id BIGINT UNSIGNED NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
variation_id BIGINT UNSIGNED NOT NULL,
customer_id BIGINT UNSIGNED NULL,
order_item_id bigint(20) unsigned NOT NULL,
order_id bigint(20) unsigned NOT NULL,
product_id bigint(20) unsigned NOT NULL,
variation_id bigint(20) unsigned NOT NULL,
customer_id bigint(20) unsigned NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
product_qty INT NOT NULL,
product_qty int(11) NOT NULL,
product_net_revenue double DEFAULT 0 NOT NULL,
product_gross_revenue double DEFAULT 0 NOT NULL,
coupon_amount double DEFAULT 0 NOT NULL,
@ -1352,8 +1356,8 @@ CREATE TABLE {$wpdb->prefix}wc_order_product_lookup (
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
order_id BIGINT UNSIGNED NOT NULL,
tax_rate_id BIGINT UNSIGNED NOT NULL,
order_id bigint(20) unsigned NOT NULL,
tax_rate_id bigint(20) unsigned NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
shipping_tax double DEFAULT 0 NOT NULL,
order_tax double DEFAULT 0 NOT NULL,
@ -1363,8 +1367,8 @@ CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
order_id BIGINT UNSIGNED NOT NULL,
coupon_id BIGINT NOT NULL,
order_id bigint(20) unsigned NOT NULL,
coupon_id bigint(20) NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
discount_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id, coupon_id),
@ -1372,7 +1376,7 @@ CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_notes (
note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
note_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
type varchar(20) NOT NULL,
locale varchar(20) NOT NULL,
@ -1383,17 +1387,17 @@ CREATE TABLE {$wpdb->prefix}wc_admin_notes (
source varchar(200) NOT NULL,
date_created datetime NOT NULL default '0000-00-00 00:00:00',
date_reminder datetime NULL default null,
is_snoozable boolean DEFAULT 0 NOT NULL,
is_snoozable tinyint(1) DEFAULT 0 NOT NULL,
layout varchar(20) DEFAULT '' NOT NULL,
image varchar(200) NULL DEFAULT NULL,
is_deleted boolean DEFAULT 0 NOT NULL,
is_read boolean DEFAULT 0 NOT NULL,
is_deleted tinyint(1) DEFAULT 0 NOT NULL,
is_read tinyint(1) DEFAULT 0 NOT NULL,
icon varchar(200) NOT NULL default 'info',
PRIMARY KEY (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_note_actions (
action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
note_id BIGINT UNSIGNED NOT NULL,
action_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
note_id bigint(20) unsigned NOT NULL,
name varchar(255) NOT NULL,
label varchar(255) NOT NULL,
query longtext NOT NULL,
@ -1405,8 +1409,8 @@ CREATE TABLE {$wpdb->prefix}wc_admin_note_actions (
KEY note_id (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_customer_lookup (
customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED DEFAULT NULL,
customer_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
user_id bigint(20) unsigned DEFAULT NULL,
username varchar(60) DEFAULT '' NOT NULL,
first_name varchar(255) NOT NULL,
last_name varchar(255) NOT NULL,
@ -1422,8 +1426,8 @@ CREATE TABLE {$wpdb->prefix}wc_customer_lookup (
KEY email (email)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_category_lookup (
category_tree_id BIGINT UNSIGNED NOT NULL,
category_id BIGINT UNSIGNED NOT NULL,
category_tree_id bigint(20) unsigned NOT NULL,
category_id bigint(20) unsigned NOT NULL,
PRIMARY KEY (category_tree_id,category_id)
) $collate;
";

View File

@ -387,6 +387,43 @@ class WC_Post_Types {
'name' => 'Product name',
),
),
array(
'core/columns',
array(),
array(
array(
'core/column',
array(
'templateLock' => 'all',
),
array(
array(
'woocommerce/product-pricing',
array(
'name' => 'regular_price',
'label' => __( 'List price', 'woocommerce' ),
'showPricingSection' => true,
),
),
),
),
array(
'core/column',
array(
'templateLock' => 'all',
),
array(
array(
'woocommerce/product-pricing',
array(
'name' => 'sale_price',
'label' => __( 'Sale price', 'woocommerce' ),
),
),
),
),
),
),
),
),
),

View File

@ -28,6 +28,16 @@ class WC_Settings_Tracking {
*/
protected $updated_options = array();
/**
* Toggled options.
*
* @var array
*/
protected $toggled_options = array(
'enabled' => array(),
'disabled' => array(),
);
/**
* Init tracking.
*/
@ -81,6 +91,12 @@ class WC_Settings_Tracking {
return;
}
// Check and save toggled options.
if ( in_array( $new_value, array( 'yes', 'no' ), true ) && in_array( $old_value, array( 'yes', 'no' ), true ) ) {
$option_state = 'yes' === $new_value ? 'enabled' : 'disabled';
$this->toggled_options[ $option_state ][] = $option_name;
}
$this->updated_options[] = $option_name;
}
@ -98,13 +114,15 @@ class WC_Settings_Tracking {
'settings' => implode( ',', $this->updated_options ),
);
if ( isset( $current_tab ) ) {
$properties['tab'] = $current_tab;
foreach ( $this->toggled_options as $state => $options ) {
if ( ! empty( $options ) ) {
$properties[ $state ] = implode( ',', $options );
}
if ( isset( $current_section ) ) {
$properties['section'] = $current_section;
}
$properties['tab'] = $current_tab ?? '';
$properties['section'] = $current_section ?? '';
WC_Tracks::record_event( 'settings_change', $properties );
}

View File

@ -168,6 +168,16 @@ class PaymentGatewaySuggestions extends \WC_REST_Data_Controller {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'transaction_processors' => array(
'description' => __( 'Array of transaction processors and their images.', 'woocommerce' ),
'type' => 'object',
'addtionalProperties' => array(
'type' => 'string',
'format' => 'uri',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);

View File

@ -84,7 +84,19 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'woocommerce_new_customer', array( __CLASS__, 'update_registered_customer' ) );
add_action( 'woocommerce_update_customer', array( __CLASS__, 'update_registered_customer' ) );
add_action( 'profile_update', array( __CLASS__, 'update_registered_customer' ) );
add_action( 'added_user_meta', array( __CLASS__, 'update_registered_customer_via_last_active' ), 10, 3 );
add_action( 'updated_user_meta', array( __CLASS__, 'update_registered_customer_via_last_active' ), 10, 3 );
add_action( 'delete_user', array( __CLASS__, 'delete_customer_by_user_id' ) );
add_action( 'remove_user_from_blog', array( __CLASS__, 'delete_customer_by_user_id' ) );
add_action( 'woocommerce_privacy_remove_order_personal_data', array( __CLASS__, 'anonymize_customer' ) );
add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 15, 2 );
}
@ -775,6 +787,20 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $results;
}
/**
* Update the database if the "last active" meta value was changed.
* Function expects to be hooked into the `added_user_meta` and `updated_user_meta` actions.
*
* @param int $meta_id ID of updated metadata entry.
* @param int $user_id ID of the user being updated.
* @param string $meta_key Meta key being updated.
*/
public static function update_registered_customer_via_last_active( $meta_id, $user_id, $meta_key ) {
if ( 'wc_last_active' === $meta_key ) {
self::update_registered_customer( $user_id );
}
}
/**
* Check if a user ID is a valid customer or other user role with past orders.
*
@ -835,6 +861,11 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function delete_customer_by_user_id( $user_id ) {
global $wpdb;
if ( (int) $user_id < 1 || doing_action( 'wp_uninitialize_site' ) ) {
// Skip the deletion.
return;
}
$user_id = (int) $user_id;
$num_deleted = $wpdb->delete( self::get_db_table_name(), array( 'user_id' => $user_id ) );
@ -843,6 +874,59 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
}
/**
* Anonymize the customer data for a single order.
*
* @internal
* @param int $order_id Order id.
* @return void
*/
public static function anonymize_customer( $order_id ) {
global $wpdb;
$customer_id = $wpdb->get_var(
$wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d", $order_id )
);
if ( ! $customer_id ) {
return;
}
// Long form query because $wpdb->update rejects [deleted].
$deleted_text = __( '[deleted]', 'woocommerce' );
$updated = $wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->prefix}wc_customer_lookup
SET
user_id = NULL,
username = %s,
first_name = %s,
last_name = %s,
email = %s,
country = '',
postcode = %s,
city = %s,
state = %s
WHERE
customer_id = %d",
array(
$deleted_text,
$deleted_text,
$deleted_text,
'deleted@site.invalid',
$deleted_text,
$deleted_text,
$deleted_text,
$customer_id,
)
)
);
// If the customer row was anonymized, flush the cache.
if ( $updated ) {
ReportsCache::invalidate();
}
}
/**
* Initialize query objects.
*/

View File

@ -1306,10 +1306,9 @@ class DataStore extends SqlQuery {
* based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Database table name.
* @return array
*/
protected function get_attribute_subqueries( $query_args, $table_name ) {
protected function get_attribute_subqueries( $query_args ) {
global $wpdb;
$sql_clauses = array(
@ -1364,10 +1363,10 @@ class DataStore extends SqlQuery {
}
$join_alias = 'orderitemmeta1';
$table_to_join_on = "{$wpdb->prefix}wc_order_product_lookup";
if ( empty( $sql_clauses['join'] ) ) {
$table_name = esc_sql( $table_name );
$sql_clauses['join'][] = "JOIN {$wpdb->prefix}woocommerce_order_items orderitems ON orderitems.order_id = {$table_name}.order_id";
$sql_clauses['join'][] = "JOIN {$wpdb->prefix}woocommerce_order_items orderitems ON orderitems.order_id = {$table_to_join_on}.order_id";
}
// If we're matching all filters (AND), we'll need multiple JOINs on postmeta.
@ -1375,7 +1374,7 @@ class DataStore extends SqlQuery {
if ( 'AND' === $match_operator || 1 === count( $sql_clauses['join'] ) ) {
$join_idx = count( $sql_clauses['join'] );
$join_alias = 'orderitemmeta' . $join_idx;
$sql_clauses['join'][] = "JOIN {$wpdb->prefix}woocommerce_order_itemmeta as {$join_alias} ON {$join_alias}.order_item_id = orderitems.order_item_id";
$sql_clauses['join'][] = "JOIN {$wpdb->prefix}woocommerce_order_itemmeta as {$join_alias} ON {$join_alias}.order_item_id = {$table_to_join_on}.order_item_id";
}
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

View File

@ -191,7 +191,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$where_subquery[] = "{$order_tax_lookup_table}.tax_rate_id NOT IN ({$excluded_tax_rates}) OR {$order_tax_lookup_table}.tax_rate_id IS NULL";
}
$attribute_subqueries = $this->get_attribute_subqueries( $query_args, $order_stats_lookup_table );
$attribute_subqueries = $this->get_attribute_subqueries( $query_args );
if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );

View File

@ -208,7 +208,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
);
// Product attribute filters.
$attribute_subqueries = $this->get_attribute_subqueries( $query_args, $orders_stats_table );
$attribute_subqueries = $this->get_attribute_subqueries( $query_args );
if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
// Build a subquery for getting order IDs by product attribute(s).
// Done here since our use case is a little more complicated than get_object_where_filter() can handle.

View File

@ -119,7 +119,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*/
protected function get_order_item_by_attribute_subquery( $query_args ) {
$order_product_lookup_table = self::get_db_table_name();
$attribute_subqueries = $this->get_attribute_subqueries( $query_args, $order_product_lookup_table );
$attribute_subqueries = $this->get_attribute_subqueries( $query_args );
if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
// Perform a subquery for DISTINCT order items that match our attribute filters.

View File

@ -47,6 +47,263 @@ class DefaultPaymentGateways {
*/
public static function get_all() {
$payment_gateways = array(
array(
'id' => 'affirm',
'title' => __( 'Affirm', 'woocommerce' ),
'content' => __( 'Affirms tailored Buy Now Pay Later programs remove price as a barrier, turning browsers into buyers, increasing average order value, and expanding your customer base.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
'plugins' => array(),
'external_link' => 'https://woocommerce.com/products/woocommerce-gateway-affirm',
'is_visible' => array(
self::get_rules_for_countries(
array(
'US',
'CA',
)
),
),
'category_other' => array(),
'category_additional' => array(
'US',
'CA',
),
),
array(
'id' => 'afterpay',
'title' => __( 'Afterpay', 'woocommerce' ),
'content' => __( 'Afterpay allows customers to receive products immediately and pay for purchases over four installments, always interest-free.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
'plugins' => array( 'afterpay-gateway-for-woocommerce' ),
'is_visible' => array(
self::get_rules_for_countries(
array(
'US',
'CA',
'AU',
)
),
),
'category_other' => array(),
'category_additional' => array(
'US',
'CA',
'AU',
),
),
array(
'id' => 'amazon_payments_advanced',
'title' => __( 'Amazon Pay', 'woocommerce' ),
'content' => __( 'Enable a familiar, fast checkout for hundreds of millions of active Amazon customers globally.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
'plugins' => array( 'woocommerce-gateway-amazon-payments-advanced' ),
'is_visible' => array(
self::get_rules_for_countries(
array(
'US',
'AT',
'BE',
'CY',
'DK',
'ES',
'FR',
'DE',
'GB',
'HU',
'IE',
'IT',
'LU',
'NL',
'PT',
'SL',
'SE',
'JP',
)
),
),
'category_other' => array(),
'category_additional' => array(
'US',
'AT',
'BE',
'CY',
'DK',
'ES',
'FR',
'DE',
'GB',
'HU',
'IE',
'IT',
'LU',
'NL',
'PT',
'SL',
'SE',
'JP',
),
),
array(
'id' => 'bacs',
'title' => __( 'Direct bank transfer', 'woocommerce' ),
'content' => __( 'Take payments via bank transfer.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/bacs.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/bacs.png',
'is_visible' => array(
self::get_rules_for_cbd( false ),
),
'is_offline' => true,
),
array(
'id' => 'cod',
'title' => __( 'Cash on delivery', 'woocommerce' ),
'content' => __( 'Take payments in cash upon delivery.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/cod.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/cod.png',
'is_visible' => array(
self::get_rules_for_cbd( false ),
),
'is_offline' => true,
),
array(
'id' => 'eway',
'title' => __( 'Eway', 'woocommerce' ),
'content' => __( 'The Eway extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/eway.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/eway.png',
'plugins' => array( 'woocommerce-gateway-eway' ),
'is_visible' => array(
self::get_rules_for_countries(
array(
'NZ',
'HK',
'SG',
'AU',
)
),
self::get_rules_for_cbd( false ),
),
'category_other' => array(
'NZ',
'HK',
'SG',
'AU',
),
'category_additional' => array(),
),
array(
'id' => 'kco',
'title' => __( 'Klarna Checkout', 'woocommerce' ),
'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
'plugins' => array( 'klarna-checkout-for-woocommerce' ),
'is_visible' => array(
self::get_rules_for_countries(
array(
'NO',
'SE',
'FI',
)
),
self::get_rules_for_cbd( false ),
),
'category_other' => array(
'NO',
'SE',
'FI',
),
'category_additional' => array(),
),
array(
'id' => 'klarna_payments',
'title' => __( 'Klarna Payments', 'woocommerce' ),
'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
'plugins' => array( 'klarna-payments-for-woocommerce' ),
'is_visible' => array(
self::get_rules_for_countries(
array(
'AT',
'BE',
'CH',
'DK',
'ES',
'FI',
'FR',
'DE',
'GB',
'IT',
'NL',
'NO',
'PL',
'SE',
)
),
self::get_rules_for_cbd( false ),
),
'category_other' => array(),
'category_additional' => array(
'US',
'CA',
'AT',
'BE',
'CH',
'DK',
'ES',
'FI',
'FR',
'DE',
'GB',
'IT',
'NL',
'NO',
'PL',
'SE',
),
),
array(
'id' => 'mollie_wc_gateway_banktransfer',
'title' => __( 'Mollie', 'woocommerce' ),
'content' => __( 'Effortless payments by Mollie: Offer global and local payment methods, get onboarded in minutes, and supported in your language.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mollie.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mollie.png',
'plugins' => array( 'mollie-payments-for-woocommerce' ),
'is_visible' => array(
self::get_rules_for_countries(
array(
'AT',
'BE',
'CH',
'ES',
'FI',
'FR',
'DE',
'GB',
'IT',
'NL',
'PL',
)
),
),
'category_other' => array(
'AT',
'BE',
'CH',
'ES',
'FI',
'FR',
'DE',
'GB',
'IT',
'NL',
'PL',
),
'category_additional' => array(),
),
array(
'id' => 'payfast',
'title' => __( 'PayFast', 'woocommerce' ),
@ -55,27 +312,10 @@ class DefaultPaymentGateways {
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payfast.png',
'plugins' => array( 'woocommerce-payfast-gateway' ),
'is_visible' => array(
self::get_rules_for_countries( array( 'ZA', 'GH', 'NG' ) ),
self::get_rules_for_countries( array( 'ZA' ) ),
self::get_rules_for_cbd( false ),
),
'category_other' => array( 'ZA', 'GH', 'NG' ),
'category_additional' => array(),
),
array(
'id' => 'stripe',
'title' => __( ' Stripe', 'woocommerce' ),
'content' => __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/stripe.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/stripe.png',
'plugins' => array( 'woocommerce-gateway-stripe' ),
'is_visible' => array(
// https://stripe.com/global.
self::get_rules_for_countries(
array( 'AU', 'AT', 'BE', 'BG', 'BR', 'CA', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HK', 'IN', 'IE', 'IT', 'JP', 'LV', 'LT', 'LU', 'MY', 'MT', 'MX', 'NL', 'NZ', 'NO', 'PL', 'PT', 'RO', 'SG', 'SK', 'SI', 'ES', 'SE', 'CH', 'GB', 'US', 'PR', 'HU', 'SL', 'ID', 'MY', 'SI', 'PR' )
),
self::get_rules_for_cbd( false ),
),
'category_other' => array( 'AU', 'AT', 'BE', 'BG', 'BR', 'CA', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HK', 'IN', 'IE', 'IT', 'JP', 'LV', 'LT', 'LU', 'MY', 'MT', 'MX', 'NL', 'NZ', 'NO', 'PL', 'PT', 'RO', 'SG', 'SK', 'SI', 'ES', 'SE', 'CH', 'GB', 'US', 'PR', 'HU', 'SL', 'ID', 'MY', 'SI', 'PR' ),
'category_other' => array( 'ZA' ),
'category_additional' => array(),
),
array(
@ -93,62 +333,21 @@ class DefaultPaymentGateways {
'category_additional' => array(),
),
array(
'id' => 'kco',
'title' => __( 'Klarna Checkout', 'woocommerce' ),
'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
'plugins' => array( 'klarna-checkout-for-woocommerce' ),
'id' => 'payubiz',
'title' => __( 'PayU for WooCommerce', 'woocommerce' ),
'content' => __( 'Enable PayUs exclusive plugin for WooCommerce to start accepting payments in 100+ payment methods available in India including credit cards, debit cards, UPI, & more!', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/payu.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payu.png',
'plugins' => array( 'payu-india' ),
'is_visible' => array(
self::get_rules_for_countries( array( 'SE', 'FI', 'NO' ) ),
self::get_rules_for_cbd( false ),
),
'category_other' => array( 'SE', 'FI', 'NO' ),
'category_additional' => array(),
),
array(
'id' => 'klarna_payments',
'title' => __( 'Klarna Payments', 'woocommerce' ),
'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
'plugins' => array( 'klarna-payments-for-woocommerce' ),
'is_visible' => array(
self::get_rules_for_countries(
array( 'DK', 'DE', 'AT', 'NL', 'CH', 'BE', 'SP', 'PL', 'FR', 'IT', 'GB', 'ES', 'FI', 'NO', 'SE', 'ES', 'FI', 'NO', 'SE' )
(object) array(
'type' => 'base_location_country',
'value' => 'IN',
'operation' => '=',
),
self::get_rules_for_cbd( false ),
),
'category_other' => array(),
'category_additional' => array( 'DK', 'DE', 'AT', 'NL', 'CH', 'BE', 'SP', 'PL', 'FR', 'IT', 'GB', 'ES', 'FI', 'NO', 'SE', 'ES', 'FI', 'NO', 'SE' ),
),
array(
'id' => 'mollie_wc_gateway_banktransfer',
'title' => __( 'Mollie', 'woocommerce' ),
'content' => __( 'Effortless payments by Mollie: Offer global and local payment methods, get onboarded in minutes, and supported in your language.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mollie.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mollie.png',
'plugins' => array( 'mollie-payments-for-woocommerce' ),
'is_visible' => array(
self::get_rules_for_countries(
array( 'FR', 'DE', 'GB', 'AT', 'CH', 'ES', 'IT', 'PL', 'FI', 'NL', 'BE' )
),
),
'category_other' => array( 'FR', 'DE', 'GB', 'AT', 'CH', 'ES', 'IT', 'PL', 'FI', 'NL', 'BE' ),
'category_additional' => array(),
),
array(
'id' => 'woo-mercado-pago-custom',
'title' => __( 'Mercado Pago Checkout Pro & Custom', 'woocommerce' ),
'content' => __( 'Accept credit and debit cards, offline (cash or bank transfer) and logged-in payments with money in Mercado Pago. Safe and secure payments with the leading payment processor in LATAM.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mercadopago.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mercadopago.png',
'plugins' => array( 'woocommerce-mercadopago' ),
'is_visible' => array(
self::get_rules_for_countries( array( 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ) ),
),
'is_local_partner' => true,
'category_other' => array( 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ),
'category_other' => array( 'IN' ),
'category_additional' => array(),
),
array(
@ -166,30 +365,304 @@ class DefaultPaymentGateways {
),
self::get_rules_for_cbd( false ),
),
'category_other' => array( 'US', 'CA', 'AT', 'BE', 'BG', 'HR', 'CH', 'CY', 'CZ', 'DK', 'EE', 'ES', 'FI', 'FR', 'DE', 'GB', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SL', 'SE', 'MX', 'BR', 'AR', 'CL', 'CO', 'EC', 'PE', 'UY', 'VE', 'AU', 'NZ', 'HK', 'JP', 'SG', 'CN', 'ID', 'ZA', 'NG', 'GH' ),
'category_additional' => array( 'US', 'CA', 'AT', 'BE', 'BG', 'HR', 'CH', 'CY', 'CZ', 'DK', 'EE', 'ES', 'FI', 'FR', 'DE', 'GB', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SL', 'SE', 'MX', 'BR', 'AR', 'CL', 'CO', 'EC', 'PE', 'UY', 'VE', 'AU', 'NZ', 'HK', 'JP', 'SG', 'CN', 'ID', 'IN', 'ZA', 'NG', 'GH' ),
'category_other' => array(
'US',
'CA',
'MX',
'BR',
'AR',
'CL',
'CO',
'EC',
'PE',
'UY',
'VE',
'AT',
'BE',
'BG',
'HR',
'CH',
'CY',
'CZ',
'DK',
'EE',
'ES',
'FI',
'FR',
'DE',
'GB',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'SK',
'SL',
'SE',
'AU',
'NZ',
'HK',
'JP',
'SG',
'CN',
'ID',
'IN',
),
'category_additional' => array(
'US',
'CA',
'ZA',
'NG',
'GH',
'EC',
'VE',
'AR',
'CL',
'CO',
'PE',
'UY',
'MX',
'BR',
'AT',
'BE',
'BG',
'HR',
'CH',
'CY',
'CZ',
'DK',
'EE',
'ES',
'FI',
'FR',
'DE',
'GB',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'SK',
'SL',
'SE',
'AU',
'NZ',
'HK',
'JP',
'SG',
'CN',
'ID',
),
),
array(
'id' => 'cod',
'title' => __( 'Cash on delivery', 'woocommerce' ),
'content' => __( 'Take payments in cash upon delivery.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/cod.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/cod.png',
'id' => 'razorpay',
'title' => __( 'Razorpay', 'woocommerce' ),
'content' => __( 'The official Razorpay extension for WooCommerce allows you to accept credit cards, debit cards, netbanking, wallet, and UPI payments.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/razorpay.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/razorpay.png',
'plugins' => array( 'woo-razorpay' ),
'is_visible' => array(
(object) array(
'type' => 'base_location_country',
'value' => 'IN',
'operation' => '=',
),
self::get_rules_for_cbd( false ),
),
'is_offline' => true,
'category_other' => array( 'IN' ),
'category_additional' => array(),
),
array(
'id' => 'bacs',
'title' => __( 'Direct bank transfer', 'woocommerce' ),
'content' => __( 'Take payments via bank transfer.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/bacs.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/bacs.png',
'id' => 'square_credit_card',
'title' => __( 'Square', 'woocommerce' ),
'content' => __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/square-black.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/square.png',
'plugins' => array( 'woocommerce-square' ),
'is_visible' => array(
(object) array(
'type' => 'or',
'operands' => (object) array(
array(
self::get_rules_for_countries( array( 'US' ) ),
self::get_rules_for_cbd( true ),
),
array(
self::get_rules_for_countries(
array(
'US',
'CA',
'IE',
'ES',
'FR',
'GB',
'AU',
'JP',
)
),
self::get_rules_for_selling_venues( array( 'brick-mortar', 'brick-mortar-other' ) ),
),
),
),
),
'category_other' => array(
'US',
'CA',
'IE',
'ES',
'FR',
'GB',
'AU',
'JP',
),
'category_additional' => array(),
),
array(
'id' => 'stripe',
'title' => __( ' Stripe', 'woocommerce' ),
'content' => __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/stripe.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/stripe.png',
'plugins' => array( 'woocommerce-gateway-stripe' ),
'is_visible' => array(
// https://stripe.com/global.
self::get_rules_for_countries(
array(
'US',
'CA',
'MX',
'BR',
'AT',
'BE',
'BG',
'CH',
'CY',
'CZ',
'DK',
'EE',
'ES',
'FI',
'FR',
'DE',
'GB',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'SK',
'SL',
'SE',
'AU',
'NZ',
'HK',
'JP',
'SG',
'ID',
'IN',
)
),
self::get_rules_for_cbd( false ),
),
'is_offline' => true,
'category_other' => array(
'US',
'CA',
'MX',
'BR',
'AT',
'BE',
'BG',
'CH',
'CY',
'CZ',
'DK',
'EE',
'ES',
'FI',
'FR',
'DE',
'GB',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'SK',
'SL',
'SE',
'AU',
'NZ',
'HK',
'JP',
'SG',
'ID',
'IN',
),
'category_additional' => array(),
),
array(
'id' => 'woo-mercado-pago-custom',
'title' => __( 'Mercado Pago Checkout Pro & Custom', 'woocommerce' ),
'content' => __( 'Accept credit and debit cards, offline (cash or bank transfer) and logged-in payments with money in Mercado Pago. Safe and secure payments with the leading payment processor in LATAM.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mercadopago.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mercadopago.png',
'plugins' => array( 'woocommerce-mercadopago' ),
'is_visible' => array(
self::get_rules_for_countries(
array(
'AR',
'CL',
'CO',
'PE',
'UY',
'MX',
'BR',
)
),
),
'is_local_partner' => true,
'category_other' => array(
'AR',
'CL',
'CO',
'PE',
'UY',
'MX',
'BR',
),
'category_additional' => array(),
),
// This is for backwards compatibility only (WC < 5.10.0-dev or WCA < 2.9.0-dev).
array(
@ -303,121 +776,6 @@ class DefaultPaymentGateways {
),
),
),
array(
'id' => 'razorpay',
'title' => __( 'Razorpay', 'woocommerce' ),
'content' => __( 'The official Razorpay extension for WooCommerce allows you to accept credit cards, debit cards, netbanking, wallet, and UPI payments.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/razorpay.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/razorpay.png',
'plugins' => array( 'woo-razorpay' ),
'is_visible' => array(
(object) array(
'type' => 'base_location_country',
'value' => 'IN',
'operation' => '=',
),
self::get_rules_for_cbd( false ),
),
'category_other' => array( 'IN' ),
'category_additional' => array(),
),
array(
'id' => 'payubiz',
'title' => __( 'PayU for WooCommerce', 'woocommerce' ),
'content' => __( 'Enable PayUs exclusive plugin for WooCommerce to start accepting payments in 100+ payment methods available in India including credit cards, debit cards, UPI, & more!', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/payu.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payu.png',
'plugins' => array( 'payu-india' ),
'is_visible' => array(
(object) array(
'type' => 'base_location_country',
'value' => 'IN',
'operation' => '=',
),
self::get_rules_for_cbd( false ),
),
'category_other' => array( 'IN' ),
'category_additional' => array(),
),
array(
'id' => 'eway',
'title' => __( 'Eway', 'woocommerce' ),
'content' => __( 'The Eway extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/eway.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/eway.png',
'plugins' => array( 'woocommerce-gateway-eway' ),
'is_visible' => array(
self::get_rules_for_countries( array( 'AU', 'NZ' ) ),
self::get_rules_for_cbd( false ),
),
'category_other' => array( 'AU', 'NZ' ),
'category_additional' => array(),
),
array(
'id' => 'square_credit_card',
'title' => __( 'Square', 'woocommerce' ),
'content' => __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/square-black.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/square.png',
'plugins' => array( 'woocommerce-square' ),
'is_visible' => array(
(object) array(
'type' => 'or',
'operands' => (object) array(
array(
self::get_rules_for_countries( array( 'US' ) ),
self::get_rules_for_cbd( true ),
),
array(
self::get_rules_for_countries( array( 'US', 'CA', 'JP', 'GB', 'AU', 'IE', 'FR', 'ES', 'FI' ) ),
self::get_rules_for_selling_venues( array( 'brick-mortar', 'brick-mortar-other' ) ),
),
),
),
),
'category_other' => array( 'US', 'CA', 'JP', 'GB', 'AU', 'IE', 'FR', 'ES', 'FI' ),
'category_additional' => array(),
),
array(
'id' => 'afterpay',
'title' => __( 'Afterpay', 'woocommerce' ),
'content' => __( 'Afterpay allows customers to receive products immediately and pay for purchases over four installments, always interest-free.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
'plugins' => array( 'afterpay-gateway-for-woocommerce' ),
'is_visible' => array(
self::get_rules_for_countries( array( 'US', 'CA' ) ),
),
'category_other' => array(),
'category_additional' => array( 'US', 'CA' ),
),
array(
'id' => 'amazon_payments_advanced',
'title' => __( 'Amazon Pay', 'woocommerce' ),
'content' => __( 'Enable a familiar, fast checkout for hundreds of millions of active Amazon customers globally.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
'plugins' => array( 'woocommerce-gateway-amazon-payments-advanced' ),
'is_visible' => array(
self::get_rules_for_countries( array( 'US', 'GB', 'JP', 'AT', 'BE', 'CY', 'DK', 'ES', 'FR', 'DE', 'HU', 'IE', 'IT', 'LU', 'NL', 'PT', 'SL', 'SE' ) ),
),
'category_other' => array(),
'category_additional' => array( 'US', 'GB', 'JP', 'AT', 'BE', 'CY', 'DK', 'ES', 'FR', 'DE', 'HU', 'IE', 'IT', 'LU', 'NL', 'PT', 'SL', 'SE' ),
),
array(
'id' => 'affirm',
'title' => __( 'Affirm', 'woocommerce' ),
'content' => __( 'Affirms tailored Buy Now Pay Later programs remove price as a barrier, turning browsers into buyers, increasing average order value, and expanding your customer base.', 'woocommerce' ),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
'plugins' => array(),
'external_link' => 'https://woocommerce.com/products/woocommerce-gateway-affirm',
'is_visible' => array(
self::get_rules_for_countries( array( 'US', 'CA' ) ),
),
'category_other' => array(),
'category_additional' => array( 'US', 'CA' ),
),
);
foreach ( $payment_gateways as $index => $payment_gateway ) {

View File

@ -27,13 +27,6 @@ class CustomersScheduler extends ImportScheduler {
* @internal
*/
public static function init() {
add_action( 'woocommerce_new_customer', array( __CLASS__, 'schedule_import' ) );
add_action( 'woocommerce_update_customer', array( __CLASS__, 'schedule_import' ) );
add_action( 'updated_user_meta', array( __CLASS__, 'schedule_import_via_last_active' ), 10, 3 );
add_action( 'woocommerce_privacy_remove_order_personal_data', array( __CLASS__, 'schedule_anonymize' ) );
add_action( 'delete_user', array( __CLASS__, 'schedule_user_delete' ) );
add_action( 'remove_user_from_blog', array( __CLASS__, 'schedule_user_delete' ) );
CustomersDataStore::init();
parent::init();
}
@ -47,8 +40,6 @@ class CustomersScheduler extends ImportScheduler {
public static function get_dependencies() {
return array(
'delete_batch_init' => OrdersScheduler::get_action( 'delete_batch_init' ),
'anonymize' => self::get_action( 'import' ),
'delete_user' => self::get_action( 'import' ),
);
}
@ -120,74 +111,6 @@ class CustomersScheduler extends ImportScheduler {
return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_customer_lookup" );
}
/**
* Get all available scheduling actions.
* Used to determine action hook names and clear events.
*
* @internal
* @return array
*/
public static function get_scheduler_actions() {
$actions = parent::get_scheduler_actions();
$actions['anonymize'] = 'wc-admin_anonymize_' . static::$name;
$actions['delete_user'] = 'wc-admin_delete_user_' . static::$name;
return $actions;
}
/**
* Schedule import.
*
* @internal
* @param int $user_id User ID.
* @return void
*/
public static function schedule_import( $user_id ) {
self::schedule_action( 'import', array( $user_id ) );
}
/**
* Schedule an import if the "last active" meta value was changed.
* Function expects to be hooked into the `updated_user_meta` action.
*
* @internal
* @param int $meta_id ID of updated metadata entry.
* @param int $user_id ID of the user being updated.
* @param string $meta_key Meta key being updated.
*/
public static function schedule_import_via_last_active( $meta_id, $user_id, $meta_key ) {
if ( 'wc_last_active' === $meta_key ) {
self::schedule_import( $user_id );
}
}
/**
* Schedule an action to anonymize a single Order.
*
* @internal
* @param WC_Order $order Order object.
* @return void
*/
public static function schedule_anonymize( $order ) {
if ( is_a( $order, 'WC_Order' ) ) {
// Postpone until any pending updates are completed.
self::schedule_action( 'anonymize', array( $order->get_id() ) );
}
}
/**
* Schedule an action to delete a single User.
*
* @internal
* @param int $user_id User ID.
* @return void
*/
public static function schedule_user_delete( $user_id ) {
if ( (int) $user_id > 0 && ! doing_action( 'wp_uninitialize_site' ) ) {
// Postpone until any pending updates are completed.
self::schedule_action( 'delete_user', array( $user_id ) );
}
}
/**
* Imports a single customer.
*
@ -220,68 +143,4 @@ class CustomersScheduler extends ImportScheduler {
CustomersDataStore::delete_customer( $customer_id );
}
}
/**
* Anonymize the customer data for a single order.
*
* @internal
* @param int $order_id Order id.
* @return void
*/
public static function anonymize( $order_id ) {
global $wpdb;
$customer_id = $wpdb->get_var(
$wpdb->prepare( "SELECT customer_id FROM {$wpdb->prefix}wc_order_stats WHERE order_id = %d", $order_id )
);
if ( ! $customer_id ) {
return;
}
// Long form query because $wpdb->update rejects [deleted].
$deleted_text = __( '[deleted]', 'woocommerce' );
$updated = $wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->prefix}wc_customer_lookup
SET
user_id = NULL,
username = %s,
first_name = %s,
last_name = %s,
email = %s,
country = '',
postcode = %s,
city = %s,
state = %s
WHERE
customer_id = %d",
array(
$deleted_text,
$deleted_text,
$deleted_text,
'deleted@site.invalid',
$deleted_text,
$deleted_text,
$deleted_text,
$customer_id,
)
)
);
// If the customer row was anonymized, flush the cache.
if ( $updated ) {
ReportsCache::invalidate();
}
}
/**
* Delete the customer data for a single user.
*
* @internal
* @param int $user_id User ID.
* @return void
*/
public static function delete_user( $user_id ) {
CustomersDataStore::delete_customer_by_user_id( $user_id );
}
}

View File

@ -166,4 +166,5 @@ class WC_Tests_Install extends WC_Unit_Test_Case {
$this->assertContains( 'some_table_name', WC_Install::get_tables() );
}
}

View File

@ -104,26 +104,4 @@ class WC_Admin_Tests_API_Init extends WC_REST_Unit_Test_Case {
$this->assertEmpty( $this->queue->actions );
}
/**
* Test that updating wc_last_active triggers a customer sync.
*
* @return void
*/
public function test_other_last_active_update_customer_sync() {
// First call creates the meta key.
// These don't use wc_update_user_last_active() because the timestamps will be the same.
update_user_meta( 1, 'wc_last_active', time() - 10 );
// Second call updates it which triggers the sync.
update_user_meta( 1, 'wc_last_active', time() );
$this->assertCount( 1, $this->queue->actions );
$this->assertArraySubset(
array(
'hook' => CustomersScheduler::get_action( 'import' ),
'args' => array( 1 ),
),
$this->queue->actions[0]
);
}
}

View File

@ -207,7 +207,6 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case {
$pending_actions
);
$this->assertContains( 'wc-admin_import_orders', $pending_hooks );
$this->assertContains( 'wc-admin_import_customers', $pending_hooks );
// Cancel outstanding actions.
$request = new WP_REST_Request( 'POST', $this->endpoint . '/cancel' );
@ -226,7 +225,6 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case {
$pending_actions
);
$this->assertNotContains( 'wc-admin_import_orders', $pending_hooks );
$this->assertNotContains( 'wc-admin_import_customers', $pending_hooks );
}
/**

View File

@ -97,4 +97,12 @@ class WC_Install_Test extends \WC_Unit_Test_Case {
$this->assertContains( 'premium_support', array_keys( $plugin_row_data ) );
}
/**
* Test that dbDelta is a noop on an installed site.
*/
public function test_dbDelta_is_a_noop() {
$db_delta_result = WC_Install::create_tables();
$this->assertEmpty( $db_delta_result );
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ program
.name( 'release-post' )
.version( '0.0.1' )
.command( 'release', 'Generate release post', { isDefault: true } )
.command( 'beta', 'Generate draft beta release post' )
.command(
'contributors',
'Generate a list of contributors for a release post'

View File

@ -0,0 +1,225 @@
/**
* External dependencies
*/
import semver from 'semver';
import { writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { Logger } from 'cli-core/src/logger';
import { Command } from '@commander-js/extra-typings';
import dotenv from 'dotenv';
// @ts-expect-error - The enquirer types are incorrect.
// eslint-disable-next-line @woocommerce/dependency-group
import { Select } from 'enquirer';
/**
* Internal dependencies
*/
import { renderTemplate } from '../../lib/render-template';
import { getWordpressComAuthToken } from '../../lib/oauth-helper';
import { getEnvVar } from '../../lib/environment';
import { getMostRecentFinal } from '../../lib/github-api';
import {
getFirstTuesdayOfTheMonth,
getSecondTuesdayOfTheMonth,
} from '../../lib/dates';
import {
createWpComDraftPost,
searchForPostsByCategory,
} from '../../lib/draft-post';
const DEVELOPER_WOOCOMMERCE_SITE_ID = '96396764';
dotenv.config();
// Define the release post command
const program = new Command()
.command( 'beta' )
.description( 'CLI to automate generation of a draft beta release post.' )
.argument(
'<releaseVersion>',
'The version for this post in x.y.z-beta.n format. Ex: 7.1.0-beta.1'
)
.option(
'--releaseDate <date>',
'The date for the final release as mm-dd-yyyy, year inferred as current year, defaults to second tuesday of next month.',
getSecondTuesdayOfTheMonth(
new Date().getMonth() + 1
).toLocaleDateString( 'en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
} )
)
.option( '--outputOnly', 'Only output the post, do not publish it' )
.option(
'--tags <tags>',
'Comma separated list of tags to add to the post.',
'Releases,WooCommerce Core'
)
.option(
'--siteId <siteId>',
'For posting to a non-default site (for testing)'
)
.action( async ( releaseVersion, options ) => {
const {
outputOnly,
siteId = DEVELOPER_WOOCOMMERCE_SITE_ID,
tags,
releaseDate,
} = options;
const postTags = ( tags &&
tags.split( ',' ).map( ( tag ) => tag.trim() ) ) || [
'WooCommerce Core',
'Releases',
];
const finalReleaseDate = new Date( releaseDate );
const isOutputOnly = !! outputOnly;
const semverVersion = semver.parse( releaseVersion );
// This is supposed to be a beta post so throw if the version provided is not a beta version.
// Things we don't accept:
// * missing beta.x
// * any other kind of prerelease, e.g. rc
// * .x must be a number, so not: beta.1b or beta.1.1 but beta.1 is ok.
if (
! semverVersion ||
! semverVersion.prerelease.length ||
typeof semverVersion.prerelease[ 1 ] === 'string'
) {
throw new Error(
`Invalid current version: ${ releaseVersion }. Provide current version in x.y.z-beta.n format.`
);
} else {
const [ , prereleaseVersion ] = semverVersion.prerelease;
// Now infer the previous version, if the one you provide is beta.1 we'll need to find the last major release from
// Github releases. If what you provided is beta.2 we'll assume previous was beta.1
const previousVersion =
prereleaseVersion === 1
? ( await getMostRecentFinal() ).tag_name
: `${ semverVersion.major }.${ semverVersion.minor }.${
semverVersion.patch
}-beta.${ prereleaseVersion - 1 }`;
const semverPreviousVersion = semver.parse( previousVersion );
if ( ! semverPreviousVersion ) {
throw new Error(
`Could not parse previous version from: ${ previousVersion }`
);
}
const clientId = getEnvVar( 'WPCOM_OAUTH_CLIENT_ID', true );
const clientSecret = getEnvVar( 'WPCOM_OAUTH_CLIENT_SECRET', true );
const redirectUri =
getEnvVar( 'WPCOM_OAUTH_REDIRECT_URI' ) ||
'http://localhost:3000/oauth';
Logger.startTask(
'Getting auth token for WordPress.com (needed to find last beta post).'
);
const authToken = await getWordpressComAuthToken(
clientId,
clientSecret,
siteId,
redirectUri,
'posts'
);
Logger.endTask();
const versionSearch =
prereleaseVersion === 1
? `WooCommerce ${ semverPreviousVersion.major }.${ semverPreviousVersion.minor }.${ semverPreviousVersion.patch }`
: `WooCommerce ${ semverPreviousVersion.major }.${ semverPreviousVersion.minor } Beta ${ semverPreviousVersion.prerelease[ 1 ] }`;
Logger.startTask(
`Finding recent release posts with title: ${ versionSearch }`
);
const posts =
( await searchForPostsByCategory(
siteId,
versionSearch,
'WooCommerce Core',
authToken
) ) || [];
Logger.endTask();
const prompt = new Select( {
name: 'Previous post',
message: 'Choose the previous post to link to:',
choices: posts.length
? posts.map( ( p ) => p.title )
: [ 'No posts found - generate default link' ],
} );
const lastReleasePostTitle: string = await prompt.run();
const lastReleasePost = posts.find(
( p ) => p.title === lastReleasePostTitle
);
if ( ! lastReleasePost ) {
Logger.warn(
'Could not find previous release post, make sure to update the link in the post before publishing.'
);
}
if ( ! authToken && ! isOutputOnly ) {
throw new Error(
'Error getting auth token, check your env settings are correct.'
);
} else {
const html = await renderTemplate( 'beta-release.ejs', {
releaseDate,
betaNumber: prereleaseVersion,
version: semverVersion,
previousVersion: semverPreviousVersion,
prettyVersion: `${ semverVersion.major }.${ semverVersion.minor }.${ semverVersion.patch } Beta ${ prereleaseVersion }`,
prettyPreviousVersion: `${ semverPreviousVersion.major }.${
semverPreviousVersion.minor
}.${ semverPreviousVersion.patch }${
semverPreviousVersion.prerelease.length
? ' ' +
semverPreviousVersion.prerelease[ 0 ] +
' ' +
semverPreviousVersion.prerelease[ 1 ]
: ''
}`,
rcReleaseDate: getFirstTuesdayOfTheMonth(
finalReleaseDate.getMonth()
),
finalReleaseDate,
lastReleasePostUrl:
lastReleasePost?.URL ||
'https://developer.woocommerce.com/category/woocommerce-core-release-notes/',
} );
if ( isOutputOnly ) {
const tmpFile = join(
tmpdir(),
`beta-release-${ releaseVersion }.html`
);
await writeFile( tmpFile, html );
Logger.notice( `Output written to ${ tmpFile }` );
} else {
Logger.startTask( 'Publishing draft release post' );
await createWpComDraftPost(
siteId,
`WooCommerce ${ semverVersion.major }.${ semverVersion.minor } Beta ${ prereleaseVersion } Released`,
html,
postTags,
authToken
);
Logger.endTask();
}
}
}
} );
program.parse( process.argv );

View File

@ -150,7 +150,7 @@ const program = new Command()
let postContent;
if ( 'undefined' !== typeof options.editPostId ) {
if ( typeof options.editPostId !== 'undefined' ) {
try {
const prevPost = await fetchWpComPost(
siteId,
@ -197,7 +197,7 @@ const program = new Command()
};
const html =
'undefined' !== typeof options.editPostId
typeof options.editPostId !== 'undefined'
? editPostHTML( postContent, {
hooks: await renderTemplate(
'hooks.ejs',
@ -234,7 +234,7 @@ const program = new Command()
try {
const { URL } =
'undefined' !== typeof options.editPostId
typeof options.editPostId !== 'undefined'
? await editWpComPostContent(
siteId,
options.editPostId,

View File

@ -0,0 +1,22 @@
export const getFirstTuesdayOfTheMonth = ( month: number ): Date => {
// create a new Date object for the first day of the month
const firstDayOfMonth = new Date( new Date().getFullYear(), month, 1 );
// create a new Date object for the first Tuesday of the month
const firstTuesday = new Date( firstDayOfMonth );
firstTuesday.setDate( 1 + ( ( 2 - firstDayOfMonth.getDay() + 7 ) % 7 ) );
return firstTuesday;
};
export const getSecondTuesdayOfTheMonth = ( month: number ): Date => {
// create a new Date object for the first Tuesday of the month
const firstTuesday = getFirstTuesdayOfTheMonth( month );
// create a new Date object for the second Tuesday of the current month
const secondTuesday = new Date( firstTuesday );
secondTuesday.setDate( secondTuesday.getDate() + 7 );
return secondTuesday;
};

View File

@ -4,6 +4,14 @@
import fetch from 'node-fetch';
import { Logger } from 'cli-core/src/logger';
// Typing just the things we need from the WP.com Post object.
// (which is not the same as WP Post object or API Post object).
// See example response here: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ to add more props.
type WordpressComPost = {
title: string;
URL: string;
};
/**
* Fetch a post from WordPress.com
*
@ -41,6 +49,39 @@ export const fetchWpComPost = async (
}
};
export const searchForPostsByCategory = async (
siteId: string,
search: string,
category: string,
authToken: string
) => {
try {
const post = await fetch(
`https://public-api.wordpress.com/rest/v1.1/sites/${ siteId }/posts?${ new URLSearchParams(
{ search, category }
) }`,
{
headers: {
Authorization: `Bearer ${ authToken }`,
'Content-Type': 'application/json',
},
method: 'GET',
}
);
if ( post.status !== 200 ) {
const text = await post.text();
throw new Error( `Error creating draft post: ${ text }` );
}
return ( await post.json() ).posts as WordpressComPost[];
} catch ( e: unknown ) {
if ( e instanceof Error ) {
Logger.error( e.message );
}
}
};
/**
* Edit a post on wordpress.com
*

View File

@ -102,3 +102,16 @@ export const getContributorData = async (
headRef,
} as ContributorData;
};
export const getMostRecentFinal = async () => {
const octokit = new Octokit( {
auth: getEnvVar( 'GITHUB_ACCESS_TOKEN', true ),
} );
const release = await octokit.repos.getLatestRelease( {
owner: 'woocommerce',
repo: 'woocommerce',
} );
return release.data;
};

View File

@ -31,6 +31,7 @@
"commander": "9.4.0",
"dotenv": "^10.0.0",
"ejs": "^3.1.8",
"enquirer": "^2.3.6",
"express": "^4.18.1",
"form-data": "^4.0.0",
"lodash.shuffle": "^4.2.0",

View File

@ -0,0 +1,164 @@
<!-- wp:paragraph -->
<p>
Beta <%= betaNumber %> for the <%=
finalReleaseDate.toLocaleDateString('en-US', {month: 'long', day:
'numeric'}) %> release of WooCommerce is now available for testing! You can
either
<a
href="https://downloads.wordpress.org/plugin/woocommerce.<%= version.version %>.zip"
target="_blank"
rel="noreferrer noopener"
>download it directly from WordPress.org</a
>
or install our
<a
rel="noreferrer noopener"
href="https://woocommerce.wordpress.com/2018/07/30/woocommerce-beta-tester-2-0-0/"
target="_blank"
>WooCommerce Beta Tester Plugin</a
>.
</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 class="wp-block-heading">Highlights</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>
Since the release of
<a href="<%= lastReleasePostUrl %>"><%= prettyPreviousVersion %></a>, the
following changes have been made:
</p>
<!-- /wp:paragraph -->
<!-- wp:list -->
<ul>
<!-- wp:list-item -->
<li>List your changes here.</li>
<!-- /wp:list-item -->
</ul>
<!-- /wp:list -->
<!-- wp:paragraph -->
<p>
For the complete list, view the&nbsp;<a
href="https://github.com/woocommerce/woocommerce/blob/release/<%= version.major %>.<%= version.minor %>/plugins/woocommerce/readme.txt"
>changelog</a
>&nbsp;in the readme for this release.
</p>
<!-- /wp:paragraph -->
<!-- wp:heading {"anchor":"actions-and-filters"} -->
<h2 class="wp-block-heading" id="actions-and-filters">Actions and Filters</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>No changes introduced.</p>
<!-- /wp:paragraph -->
<!-- wp:heading {"anchor":"database-changes"} -->
<h2 class="wp-block-heading" id="database-changes">Database Changes</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>No changes introduced.</p>
<!-- /wp:paragraph -->
<!-- wp:heading {"anchor":"template-changes"} -->
<h2 class="wp-block-heading" id="template-changes">Template Changes</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>No changes introduced.</p>
<!-- /wp:paragraph -->
<!-- wp:heading {"anchor":"release-schedule"} -->
<h2 class="wp-block-heading" id="release-schedule">Release Schedule</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>
We're still on track for our planned <%=
finalReleaseDate.toLocaleDateString('en-US', {month: 'long', day:
'numeric'}) %> release.
</p>
<!-- /wp:paragraph -->
<!-- wp:table -->
<figure class="wp-block-table">
<table>
<tbody>
<tr>
<td><strong>Version</strong></td>
<td><strong>Release</strong></td>
</tr>
<tr>
<td>Release Candidate</td>
<td>
<%= rcReleaseDate.toLocaleDateString('en-US', { month:
'long', day: 'numeric', year: 'numeric' }) %>
</td>
</tr>
<tr>
<td>Final Release</td>
<td>
<%= finalReleaseDate.toLocaleDateString('en-US', { month:
'long', day: 'numeric', year: 'numeric' }) %>
</td>
</tr>
</tbody>
</table>
</figure>
<!-- /wp:table -->
<!-- wp:heading {"anchor":"testing"} -->
<h2 class="wp-block-heading" id="testing">Testing</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>
If you'd like to dive in and help test this new release, our handy&nbsp;<a
href="https://wordpress.org/plugins/woocommerce-beta-tester/"
target="_blank"
>WooCommerce Beta Tester plugin</a
>&nbsp;allows you to switch between beta versions and release candidates.
You can also&nbsp;<a
href="https://downloads.wordpress.org/plugin/woocommerce.<%= version.version %>.zip"
target="_blank"
rel="noreferrer noopener"
>download the release from WordPress.org</a
>.
</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>
A set of testing instructions has been published on&nbsp;our&nbsp;<a
href="https://github.com/woocommerce/woocommerce/wiki/Release-Testing-Instructions-WooCommerce-<%= version.major %>.<%= version.minor %>"
target="_blank"
rel="noreferrer noopener"
>Wiki page in GitHub</a
>. We've also posted&nbsp;<a
href="https://woocommerce.wordpress.com/2015/07/25/how-to-beta-test-woocommerce/"
target="_blank"
>a helpful writeup on beta testing</a
>&nbsp;to help get you started.
</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>
If you discover any bugs during the testing process, please let us know
by&nbsp;<a
href="https://github.com/woocommerce/woocommerce/issues/new?assignees=&amp;labels=&amp;template=1-bug-report.yml&amp;title=[<%= version.major %>.<%= version.minor %> beta]: Title of the issue"
target="_blank"
rel="noreferrer noopener"
>logging a report in GitHub</a
>.
</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->

View File

@ -1,7 +1,10 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"module": "Node16"
},
"ts-node": {
"transpileOnly": true,
"files": true,
"files": true
}
}