From e1e8306e6d1c1a1be47eabf2c57e38b1f3d27076 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 7 Nov 2023 14:36:36 +0100 Subject: [PATCH 01/46] move address to PHP side --- .../settings/shared/default-address-fields.ts | 122 +------------ src/Domain/Bootstrap.php | 12 +- src/Domain/Services/CheckoutFields.php | 168 ++++++++++++++++++ 3 files changed, 183 insertions(+), 119 deletions(-) create mode 100644 src/Domain/Services/CheckoutFields.php diff --git a/assets/js/settings/shared/default-address-fields.ts b/assets/js/settings/shared/default-address-fields.ts index 64c9a64ee2b..501329d525d 100644 --- a/assets/js/settings/shared/default-address-fields.ts +++ b/assets/js/settings/shared/default-address-fields.ts @@ -1,7 +1,7 @@ /** - * External dependencies + * Internal dependencies */ -import { __ } from '@wordpress/i18n'; +import { getSetting } from './utils'; export interface AddressField { // The label for the field. @@ -65,120 +65,8 @@ export type CountryAddressFields = Record< string, AddressFields >; /** * Default address field properties. */ -export const defaultAddressFields: AddressFields = { - first_name: { - label: __( 'First name', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'First name (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'given-name', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 10, - }, - last_name: { - label: __( 'Last name', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Last name (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'family-name', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 20, - }, - company: { - label: __( 'Company', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Company (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'organization', - autocapitalize: 'sentences', - required: false, - hidden: false, - index: 30, - }, - address_1: { - label: __( 'Address', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Address (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'address-line1', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 40, - }, - address_2: { - label: __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Apartment, suite, etc. (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'address-line2', - autocapitalize: 'sentences', - required: false, - hidden: false, - index: 50, - }, - country: { - label: __( 'Country/Region', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Country/Region (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'country', - required: true, - hidden: false, - index: 60, - }, - city: { - label: __( 'City', 'woo-gutenberg-products-block' ), - optionalLabel: __( 'City (optional)', 'woo-gutenberg-products-block' ), - autocomplete: 'address-level2', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 70, - }, - state: { - label: __( 'State/County', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'State/County (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'address-level1', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 80, - }, - postcode: { - label: __( 'Postal code', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Postal code (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'postal-code', - autocapitalize: 'characters', - required: true, - hidden: false, - index: 90, - }, - phone: { - label: __( 'Phone', 'woo-gutenberg-products-block' ), - optionalLabel: __( 'Phone (optional)', 'woo-gutenberg-products-block' ), - autocomplete: 'tel', - type: 'tel', - required: true, - hidden: false, - index: 100, - }, -}; +export const defaultAddressFields: AddressFields = getSetting< AddressFields >( + 'defaultAddressFields' +); export default defaultAddressFields; diff --git a/src/Domain/Bootstrap.php b/src/Domain/Bootstrap.php index 4b40fd32ee6..caf81b066f5 100644 --- a/src/Domain/Bootstrap.php +++ b/src/Domain/Bootstrap.php @@ -13,6 +13,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating; use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics; use Automattic\WooCommerce\Blocks\Domain\Services\Hydration; +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; use Automattic\WooCommerce\Blocks\InboxNotifications; use Automattic\WooCommerce\Blocks\Installer; use Automattic\WooCommerce\Blocks\Migration; @@ -131,6 +132,7 @@ function() { $this->container->get( CreateAccount::class )->init(); $this->container->get( ShippingController::class )->init(); $this->container->get( TasksController::class )->init(); + $this->container->get( CheckoutFields::class ); // Load assets in admin and on the frontend. if ( ! $is_rest ) { @@ -171,9 +173,9 @@ protected function has_core_dependencies() { if ( $has_needed_dependencies ) { $plugin_data = \get_file_data( $this->package->get_path( 'woocommerce-gutenberg-products-block.php' ), - [ + array( 'RequiredWCVersion' => 'WC requires at least', - ] + ) ); if ( isset( $plugin_data['RequiredWCVersion'] ) && version_compare( \WC()->version, $plugin_data['RequiredWCVersion'], '<' ) ) { $has_needed_dependencies = false; @@ -376,6 +378,12 @@ function( Container $container ) { return new Hydration( $container->get( AssetDataRegistry::class ) ); } ); + $this->container->register( + CheckoutFields::class, + function( Container $container ) { + return new CheckoutFields( $container->get( AssetDataRegistry::class ) ); + } + ); $this->container->register( PaymentsApi::class, function ( Container $container ) { diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php new file mode 100644 index 00000000000..fc681dd5f60 --- /dev/null +++ b/src/Domain/Services/CheckoutFields.php @@ -0,0 +1,168 @@ +asset_data_registry = $asset_data_registry; + $this->fields = array( + 'first_name' => array( + 'label' => __( 'First name', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'First name (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'given-name', + 'autocapitalize' => 'sentences', + 'index' => 10, + ), + 'last_name' => array( + 'label' => __( 'Last name', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Last name (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'family-name', + 'autocapitalize' => 'sentences', + 'index' => 20, + ), + 'company' => array( + 'label' => __( 'Company', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Company (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => false, + 'hidden' => false, + 'autocomplete' => 'organization', + 'autocapitalize' => 'sentences', + 'index' => 30, + ), + 'address_1' => array( + 'label' => __( 'Address', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Address (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'address-line1', + 'autocapitalize' => 'sentences', + 'index' => 40, + ), + 'address_2' => array( + 'label' => __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Apartment, suite, etc. (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => false, + 'hidden' => false, + 'autocomplete' => 'address-line2', + 'autocapitalize' => 'sentences', + 'index' => 50, + ), + 'country' => array( + 'label' => __( 'Country/Region', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Country/Region (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'country', + 'index' => 50, + ), + 'city' => array( + 'label' => __( 'City', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'City (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'address-level2', + 'autocapitalize' => 'sentences', + 'index' => 70, + ), + 'state' => array( + 'label' => __( 'State/County', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'State/County (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'address-level1', + 'autocapitalize' => 'sentences', + 'index' => 80, + ), + 'postcode' => array( + 'label' => __( 'Postal code', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Postal code (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'postal-code', + 'autocapitalize' => 'characters', + 'index' => 90, + ), + 'plugin_vat' => array( + 'label' => __( 'VAT', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'VAT (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => false, + 'hidden' => false, + 'autocomplete' => 'vat', + 'autocapitalize' => 'characters', + ), + ); + $this->initialize(); + } + + /** + * Initialize hooks. + */ + public function initialize() { + // @TODO: this should move to a class that only run on UI operations. + add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) ); + } + + /** + * Add fields data to the asset data registry. + */ + public function add_fields_data() { + $this->asset_data_registry->add( 'defaultAddressFields', $this->fields, true ); + } +} From 50c372a1a6b9a934a863cec74d8e5589fe313cd0 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Thu, 16 Nov 2023 16:27:43 +0100 Subject: [PATCH 02/46] support showing fields from server --- .../address-form/address-form.tsx | 14 +--- .../customer-address.tsx | 9 +-- .../customer-address.tsx | 14 +--- assets/js/settings/blocks/constants.ts | 38 ++++++++++ .../settings/shared/default-address-fields.ts | 9 ++- src/Domain/Bootstrap.php | 1 + src/Domain/Services/CheckoutFields.php | 76 +++++++++++++++++-- 7 files changed, 124 insertions(+), 37 deletions(-) diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx index aa7c8b0a3cc..b3f90a26006 100644 --- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx +++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx @@ -17,32 +17,22 @@ import { import { useEffect, useMemo, useRef } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { useShallowEqual } from '@woocommerce/base-hooks'; -import { defaultAddressFields } from '@woocommerce/settings'; import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ -import { - AddressFormProps, - FieldType, - FieldConfig, - AddressFormFields, -} from './types'; +import { AddressFormProps, FieldConfig, AddressFormFields } from './types'; import prepareAddressFields from './prepare-address-fields'; import validateShippingCountry from './validate-shipping-country'; import customValidationHandler from './custom-validation-handler'; -const defaultFields = Object.keys( - defaultAddressFields -) as unknown as FieldType[]; - /** * Checkout address form. */ const AddressForm = ( { id = '', - fields = defaultFields, + fields, fieldConfig = {} as FieldConfig, onChange, type = 'shipping', diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx index 32adfb5400d..5127012638d 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx @@ -11,6 +11,7 @@ import type { } from '@woocommerce/settings'; import { useSelect } from '@wordpress/data'; import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; +import { ADDRESS_FIELDS_KEYS } from '@woocommerce/block-settings'; /** * Internal dependencies @@ -26,7 +27,6 @@ const CustomerAddress = ( { defaultEditing?: boolean; } ) => { const { - defaultAddressFields, billingAddress, setShippingAddress, setBillingAddress, @@ -58,10 +58,6 @@ const CustomerAddress = ( { } }, [ editing, hasValidationErrors, invalidProps.length ] ); - const addressFieldKeys = Object.keys( - defaultAddressFields - ) as ( keyof AddressFields )[]; - const onChangeAddress = useCallback( ( values: Partial< BillingAddress > ) => { setBillingAddress( values ); @@ -101,13 +97,12 @@ const CustomerAddress = ( { type="billing" onChange={ onChangeAddress } values={ billingAddress } - fields={ addressFieldKeys } + fields={ ADDRESS_FIELDS_KEYS } fieldConfig={ addressFieldsConfig } /> ), [ - addressFieldKeys, addressFieldsConfig, billingAddress, onChangeAddress, diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx index cd327901d0c..ce9473b7a39 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/customer-address.tsx @@ -11,6 +11,7 @@ import type { } from '@woocommerce/settings'; import { useSelect } from '@wordpress/data'; import { VALIDATION_STORE_KEY } from '@woocommerce/block-data'; +import { ADDRESS_FIELDS_KEYS } from '@woocommerce/block-settings'; /** * Internal dependencies @@ -26,7 +27,6 @@ const CustomerAddress = ( { defaultEditing?: boolean; } ) => { const { - defaultAddressFields, shippingAddress, setShippingAddress, setBillingAddress, @@ -57,9 +57,6 @@ const CustomerAddress = ( { } }, [ editing, hasValidationErrors, invalidProps.length ] ); - const addressFieldKeys = Object.keys( - defaultAddressFields - ) as ( keyof AddressFields )[]; const onChangeAddress = useCallback( ( values: Partial< ShippingAddress > ) => { setShippingAddress( values ); @@ -98,16 +95,11 @@ const CustomerAddress = ( { type="shipping" onChange={ onChangeAddress } values={ shippingAddress } - fields={ addressFieldKeys } + fields={ ADDRESS_FIELDS_KEYS } fieldConfig={ addressFieldsConfig } /> ), - [ - addressFieldKeys, - addressFieldsConfig, - onChangeAddress, - shippingAddress, - ] + [ addressFieldsConfig, onChangeAddress, shippingAddress ] ); return ( diff --git a/assets/js/settings/blocks/constants.ts b/assets/js/settings/blocks/constants.ts index 8436f26424c..9cff6d62f28 100644 --- a/assets/js/settings/blocks/constants.ts +++ b/assets/js/settings/blocks/constants.ts @@ -57,6 +57,12 @@ type CountryData = { locale: Record< string, LocaleSpecificAddressField >; }; +type FieldsLocations = { + address: string[]; + contact: string[]; + additional: string[]; +}; + // Contains country names. const countries = getSetting< Record< string, string > >( 'countries', {} ); @@ -111,3 +117,35 @@ export const COUNTRY_LOCALE = Object.fromEntries( return [ countryCode, countryData[ countryCode ].locale || [] ]; } ) ); + +const defaultFieldsLocations: FieldsLocations = { + address: [ + 'first_name', + 'last_name', + 'company', + 'address_1', + 'address_2', + 'city', + 'postcode', + 'country', + 'state', + 'phone', + ], + contact: [ 'email' ], + additional: [], +}; + +export const ADDRESS_FIELDS_KEYS = getSetting< FieldsLocations >( + 'addressFieldsLocations', + defaultFieldsLocations +).address; + +export const CONTACT_FIELDS_KEYS = getSetting< FieldsLocations >( + 'addressFieldsLocations', + defaultFieldsLocations +).contact; + +export const ADDITIONAL_FIELDS_KEYS = getSetting< FieldsLocations >( + 'addressFieldsLocations', + defaultFieldsLocations +).additional; diff --git a/assets/js/settings/shared/default-address-fields.ts b/assets/js/settings/shared/default-address-fields.ts index 501329d525d..c4f31514cf8 100644 --- a/assets/js/settings/shared/default-address-fields.ts +++ b/assets/js/settings/shared/default-address-fields.ts @@ -26,7 +26,7 @@ export interface LocaleSpecificAddressField extends Partial< AddressField > { priority?: number | undefined; } -export interface AddressFields { +export interface CoreAddressFields { first_name: AddressField; last_name: AddressField; company: AddressField; @@ -39,8 +39,11 @@ export interface AddressFields { phone: AddressField; } +export type AddressFields = CoreAddressFields & Record< string, AddressField >; + export type AddressType = 'billing' | 'shipping'; -export interface ShippingAddress { + +export interface CoreAddress { first_name: string; last_name: string; company: string; @@ -53,6 +56,8 @@ export interface ShippingAddress { phone: string; } +export type ShippingAddress = CoreAddress & Record< string, string >; + export type KeyedAddressField = AddressField & { key: keyof AddressFields; errorMessage?: string; diff --git a/src/Domain/Bootstrap.php b/src/Domain/Bootstrap.php index caf81b066f5..0e10312de84 100644 --- a/src/Domain/Bootstrap.php +++ b/src/Domain/Bootstrap.php @@ -141,6 +141,7 @@ function() { $this->container->get( AssetsController::class ); $this->container->get( Installer::class )->init(); $this->container->get( GoogleAnalytics::class )->init(); + $this->container->get( CheckoutFields::class )->init(); } // Load assets unless this is a request specifically for the store API. diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index fc681dd5f60..afbaeb419f5 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -1,18 +1,35 @@ asset_data_registry = $asset_data_registry; - $this->fields = array( + $this->core_fields = array( + 'email' => array( + 'label' => __( 'Email address', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Email address (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => true, + 'hidden' => false, + 'autocomplete' => 'email', + 'autocapitalize' => 'none', + 'index' => 0, + ), 'first_name' => array( 'label' => __( 'First name', 'woo-gutenberg-products-block' ), 'optionalLabel' => __( @@ -136,6 +165,21 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'autocapitalize' => 'characters', 'index' => 90, ), + 'phone' => array( + 'label' => __( 'Phone', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'Phone (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => false, + 'hidden' => false, + 'autocomplete' => 'tel', + 'autocapitalize' => 'characters', + 'index' => 100, + ), + ); + + $this->additional_fields = array( 'plugin_vat' => array( 'label' => __( 'VAT', 'woo-gutenberg-products-block' ), 'optionalLabel' => __( @@ -148,13 +192,22 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'autocapitalize' => 'characters', ), ); - $this->initialize(); + + $this->fields_locations = array( + // omit email from shipping and billing fields. + 'address' => $this->get_address_fields_keys(), + // @todo handle rendering contact fields. + 'contact' => array( 'email' ), + // @todo handle rendering additional fields. + 'additional' => array(), + ); + } /** * Initialize hooks. */ - public function initialize() { + public function init() { // @TODO: this should move to a class that only run on UI operations. add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) ); } @@ -163,6 +216,19 @@ public function initialize() { * Add fields data to the asset data registry. */ public function add_fields_data() { - $this->asset_data_registry->add( 'defaultAddressFields', $this->fields, true ); + $this->asset_data_registry->add( 'defaultAddressFields', array_merge( $this->core_fields, $this->additional_fields ), true ); + $this->asset_data_registry->add( 'addressFieldsLocations', $this->fields_locations, true ); + } + + /** + * Get the keys of the address fields. + * + * @return array + */ + protected function get_address_fields_keys() { + $core_fields = array_keys( array_diff_key( $this->core_fields, array( 'email' => '' ) ) ); + $additional_fields = array( 'plugin_vat' ); + + return array_merge( $core_fields, $additional_fields ); } } From 33a07c1d1218a17a6128346abb7547a680f5b72c Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 5 Dec 2023 15:14:22 +0100 Subject: [PATCH 03/46] add placeholders to code --- src/Domain/Bootstrap.php | 2 +- src/Domain/Services/CheckoutFields.php | 148 ++++++++++++++++-- src/StoreApi/Routes/V1/CartUpdateCustomer.php | 5 + src/StoreApi/Routes/V1/Checkout.php | 6 +- src/StoreApi/Routes/V1/CheckoutOrder.php | 1 + .../Schemas/V1/AbstractAddressSchema.php | 126 +++++++-------- .../Schemas/V1/BillingAddressSchema.php | 38 +++-- src/StoreApi/Schemas/V1/CartSchema.php | 1 + src/StoreApi/Schemas/V1/CheckoutSchema.php | 7 + .../Schemas/V1/ShippingAddressSchema.php | 37 +++-- src/StoreApi/StoreApi.php | 14 +- src/StoreApi/Utilities/CheckoutTrait.php | 38 +++++ src/StoreApi/Utilities/OrderController.php | 77 ++++----- 13 files changed, 349 insertions(+), 151 deletions(-) diff --git a/src/Domain/Bootstrap.php b/src/Domain/Bootstrap.php index 0e10312de84..baab522c8eb 100644 --- a/src/Domain/Bootstrap.php +++ b/src/Domain/Bootstrap.php @@ -126,7 +126,7 @@ function() { $is_store_api_request = $is_rest && ! empty( $_SERVER['REQUEST_URI'] ) && ( false !== strpos( $_SERVER['REQUEST_URI'], trailingslashit( rest_get_url_prefix() ) . 'wc/store/' ) ); // Load and init assets. - $this->container->get( StoreApi::class )->init(); + $this->container->get( StoreApi::class )->init( $this->container->get( CheckoutFields::class ) ); $this->container->get( PaymentsApi::class )->init(); $this->container->get( DraftOrders::class )->init(); $this->container->get( CreateAccount::class )->init(); diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index afbaeb419f5..3f9e2adbb9f 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -7,7 +7,7 @@ /** * Service class managing checkout fields and its related extensibility points. */ -class CheckoutFields { +abstract class CheckoutFields { /** @@ -173,6 +173,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { ), 'required' => false, 'hidden' => false, + 'type' => 'tel', 'autocomplete' => 'tel', 'autocapitalize' => 'characters', 'index' => 100, @@ -180,14 +181,25 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { ); $this->additional_fields = array( - 'plugin_vat' => array( + 'plugin_vat' => array( 'label' => __( 'VAT', 'woo-gutenberg-products-block' ), 'optionalLabel' => __( 'VAT (optional)', 'woo-gutenberg-products-block' ), 'required' => false, - 'hidden' => false, + 'hidden' => true, + 'autocomplete' => 'vat', + 'autocapitalize' => 'characters', + ), + 'plugin_delivery_hour' => array( + 'label' => __( 'VAT', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( + 'VAT (optional)', + 'woo-gutenberg-products-block' + ), + 'required' => false, + 'hidden' => true, 'autocomplete' => 'vat', 'autocapitalize' => 'characters', ), @@ -195,13 +207,12 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { $this->fields_locations = array( // omit email from shipping and billing fields. - 'address' => $this->get_address_fields_keys(), + 'address' => $this->get_address_fields_keys(), // everything here will be saved to customer and order. // @todo handle rendering contact fields. - 'contact' => array( 'email' ), + 'contact' => array( 'email' ), // everything here will be saved to order, and optionally to customer. // @todo handle rendering additional fields. - 'additional' => array(), + 'additional' => array( 'plugin_delivery_hour' ), // everything here will only be saved to order only. ); - } /** @@ -221,14 +232,123 @@ public function add_fields_data() { } /** - * Get the keys of the address fields. + * Registers an additional field for Checkout. + * + * @param array $options The field options. * - * @return array + * @return true|WP_Error True if the field was registered, a WP_Error otherwise. */ - protected function get_address_fields_keys() { - $core_fields = array_keys( array_diff_key( $this->core_fields, array( 'email' => '' ) ) ); - $additional_fields = array( 'plugin_vat' ); + abstract public function register_checkout_field( $options ); + + /** + * Returns an array of fields for a given group. + * + * @param string $group The group to get fields for (address|contact|additional). + * + * @return array An array of fields. + */ + abstract public function get_fields_for_group( $group ); + + /** + * Returns an array of fields keys for a the address group. + * + * @return array An array of fields keys. + */ + abstract protected function get_address_fields_keys(); + + /** + * Returns an array of fields keys for a the contact group. + * + * @return array An array of fields keys. + */ + abstract protected function get_contact_fields_keys(); + + /** + * Returns an array of fields keys for a the additional area group. + * + * @return array An array of fields keys. + */ + abstract protected function get_additional_fields_keys(); + + /** + * Validates a field value for a given group. + * + * @param string $key The field key. + * @param mixed $value The field value. + * @param string $group The group to validate the field for (address|contact|additional). + * + * TODO: we might not need the group param here. + * + * @return true|\WP_Error True if the field is valid, a WP_Error otherwise. + */ + abstract public function validate_field_for_group( $key, $value, $group ); + + /** + * Returns true if the given key is a valid field. + * + * @param string $key The field key. + * + * @return bool True if the field is valid, false otherwise. + */ + abstract public function is_field( $key ); + + /** + * Persists a field value for a given order. This would also optionally set the field value on the customer. + * + * @param string $key The field key. + * @param mixed $value The field value. + * @param \WC_Order $order The order to persist the field for. + * + * @return void + */ + abstract public function persist_field( $key, $value, $order ); + + /** + * Persists a field value for a given customer. + * + * @param string $key The field key. + * @param mixed $value The field value. + * @param \WC_Customer $customer The customer to persist the field for. + * + * @return void + */ + abstract public function persist_field_for_customer( $key, $value, $customer ); + /** + * Returns a field value for a given customer. + * + * @param string $field The field key. + * @param \WC_Customer $customer The customer to get the field value for. + * + * @return mixed The field value. + */ + abstract public function get_field_from_customer( $field, $customer ); + + /** + * Returns a field value for a given order. + * + * @param string $field The field key. + * @param \WC_Order $order The order to get the field value for. + * + * @return mixed The field value. + */ + abstract public function get_field_from_order( $field, $order ); + + /** + * Returns an array of all fields values for a given customer. + * + * @param \WC_Customer $customer The customer to get the fields for. + * + * @return array An array of fields. + */ + abstract public function get_all_fields_from_customer( $customer ); + + /** + * Returns an array of all fields values for a given order. + * + * @param \WC_Order $order The order to get the fields for. + * + * @return array An array of fields. + */ + abstract public function get_all_fields_from_order( $order ); - return array_merge( $core_fields, $additional_fields ); - } } diff --git a/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/src/StoreApi/Routes/V1/CartUpdateCustomer.php index a9201b33320..df9363e1412 100644 --- a/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -182,6 +182,7 @@ protected function get_route_post_response( \WP_REST_Request $request ) { 'shipping_phone' => $shipping['phone'] ?? null, ) ); + // @TODO: add additional fields here like we did with order controller. wc_do_deprecated_action( 'woocommerce_blocks_cart_update_customer_from_request', @@ -244,6 +245,8 @@ protected function get_customer_billing_address( \WC_Customer $customer ) { 'phone' => $customer->get_billing_phone(), 'email' => $customer->get_billing_email(), ]; + // @TODO: also include additional address fields for billing. + // get_all_fields_from_customer( $customer, 'billing' ), } /** @@ -265,5 +268,7 @@ protected function get_customer_shipping_address( \WC_Customer $customer ) { 'country' => $customer->get_shipping_country(), 'phone' => $customer->get_shipping_phone(), ]; + // @TODO: also include additional address fields for shipping. + // get_all_fields_from_customer( $customer, 'shipping' ), } } diff --git a/src/StoreApi/Routes/V1/Checkout.php b/src/StoreApi/Routes/V1/Checkout.php index c19c86e9973..9a525969ede 100644 --- a/src/StoreApi/Routes/V1/Checkout.php +++ b/src/StoreApi/Routes/V1/Checkout.php @@ -94,6 +94,7 @@ public function get_args() { ], ], ], + // @TODO: add fields schema to checkout schema. $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) ), ], @@ -412,11 +413,12 @@ private function create_or_update_draft_order( \WP_REST_Request $request ) { */ private function update_customer_from_request( \WP_REST_Request $request ) { $customer = wc()->customer; - // Billing address is a required field. foreach ( $request['billing_address'] as $key => $value ) { if ( is_callable( [ $customer, "set_billing_$key" ] ) ) { $customer->{"set_billing_$key"}( $value ); + } elseif ( $this->additional_fields_controller->is_field( $key, 'address' ) ) { + $this->additional_fields_controller->persist_field_for_customer( "/billing/$key", $value, $customer ); } } @@ -428,6 +430,8 @@ private function update_customer_from_request( \WP_REST_Request $request ) { $customer->{"set_shipping_$key"}( $value ); } elseif ( 'phone' === $key ) { $customer->update_meta_data( 'shipping_phone', $value ); + } elseif ( $this->additional_fields_controller->is_field( $key, 'address' ) ) { + $this->additional_fields_controller->persist_field_for_customer( "/shipping/$key", $value, $customer ); } } diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index 48941bb2a18..a3914484de9 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -7,6 +7,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait; use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait; +// @TODO: add custom fields support. /** * CheckoutOrder class. */ diff --git a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index 77b2198cbba..352969169d9 100644 --- a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -16,68 +16,71 @@ abstract class AbstractAddressSchema extends AbstractSchema { * @return array */ public function get_properties() { - return [ - 'first_name' => [ - 'description' => __( 'First name', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, + return array_merge( + [ + 'first_name' => [ + 'description' => __( 'First name', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'last_name' => [ + 'description' => __( 'Last name', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'company' => [ + 'description' => __( 'Company', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'address_1' => [ + 'description' => __( 'Address', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'address_2' => [ + 'description' => __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'city' => [ + 'description' => __( 'City', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'state' => [ + 'description' => __( 'State/County code, or name of the state, county, province, or district.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'postcode' => [ + 'description' => __( 'Postal code', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'country' => [ + 'description' => __( 'Country/Region code in ISO 3166-1 alpha-2 format.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], + 'phone' => [ + 'description' => __( 'Phone', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ], ], - 'last_name' => [ - 'description' => __( 'Last name', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'company' => [ - 'description' => __( 'Company', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'address_1' => [ - 'description' => __( 'Address', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'address_2' => [ - 'description' => __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'city' => [ - 'description' => __( 'City', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'state' => [ - 'description' => __( 'State/County code, or name of the state, county, province, or district.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'postcode' => [ - 'description' => __( 'Postal code', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'country' => [ - 'description' => __( 'Country/Region code in ISO 3166-1 alpha-2 format.', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - 'phone' => [ - 'description' => __( 'Phone', 'woo-gutenberg-products-block' ), - 'type' => 'string', - 'context' => [ 'view', 'edit' ], - 'required' => true, - ], - ]; + $this->get_additional_address_fields_schema(), + ); } /** @@ -102,6 +105,7 @@ public function sanitize_callback( $address, $request, $param ) { $address['state'] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address['state'] ) ), $address['country'] ); $address['postcode'] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; $address['phone'] = sanitize_text_field( wp_unslash( $address['phone'] ) ); + // @TODO: sanitize additional address fields. return $address; } diff --git a/src/StoreApi/Schemas/V1/BillingAddressSchema.php b/src/StoreApi/Schemas/V1/BillingAddressSchema.php index a166113e8cd..b13df79a18e 100644 --- a/src/StoreApi/Schemas/V1/BillingAddressSchema.php +++ b/src/StoreApi/Schemas/V1/BillingAddressSchema.php @@ -99,20 +99,32 @@ public function get_item_response( $address ) { $billing_state = ''; } + // @TODO: add additional fields to the response. + if ( $address instanceof \WC_Order ) { + // get additional fields from order. + $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_order( $address ); + } elseif ( $address instanceof \WC_Customer ) { + // get additional fields from customer. + $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_customer( $address ); + } + return $this->prepare_html_response( - [ - 'first_name' => $address->get_billing_first_name(), - 'last_name' => $address->get_billing_last_name(), - 'company' => $address->get_billing_company(), - 'address_1' => $address->get_billing_address_1(), - 'address_2' => $address->get_billing_address_2(), - 'city' => $address->get_billing_city(), - 'state' => $billing_state, - 'postcode' => $address->get_billing_postcode(), - 'country' => $billing_country, - 'email' => $address->get_billing_email(), - 'phone' => $address->get_billing_phone(), - ] + \array_merge( + [ + 'first_name' => $address->get_billing_first_name(), + 'last_name' => $address->get_billing_last_name(), + 'company' => $address->get_billing_company(), + 'address_1' => $address->get_billing_address_1(), + 'address_2' => $address->get_billing_address_2(), + 'city' => $address->get_billing_city(), + 'state' => $billing_state, + 'postcode' => $address->get_billing_postcode(), + 'country' => $billing_country, + 'email' => $address->get_billing_email(), + 'phone' => $address->get_billing_phone(), + ], + $additional_address_fields + ) ); } throw new RouteException( diff --git a/src/StoreApi/Schemas/V1/CartSchema.php b/src/StoreApi/Schemas/V1/CartSchema.php index 3489c1cad67..0f1c2d86d46 100644 --- a/src/StoreApi/Schemas/V1/CartSchema.php +++ b/src/StoreApi/Schemas/V1/CartSchema.php @@ -366,6 +366,7 @@ public function get_item_response( $cart ) { 'errors' => $cart_errors, 'payment_methods' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ), self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), + ]; } diff --git a/src/StoreApi/Schemas/V1/CheckoutSchema.php b/src/StoreApi/Schemas/V1/CheckoutSchema.php index 3a84f9c0759..9e5b5e105e8 100644 --- a/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -168,6 +168,12 @@ public function get_properties() { ], ], ], + 'additional_fields' => [ + 'description' => __( 'Additional fields to be persisted on the order.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + 'context' => [ 'view', 'edit' ], + 'properties' => $this->get_additional_fields_schema(), + ], self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ), ]; } @@ -205,6 +211,7 @@ protected function get_checkout_response( \WC_Order $order, PaymentResult $payme 'payment_details' => $this->prepare_payment_details_for_response( $payment_result->payment_details ), 'redirect_url' => $payment_result->redirect_url, ], + 'additional_fields' => $this->get_additional_fields_response( $order ), self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), ]; } diff --git a/src/StoreApi/Schemas/V1/ShippingAddressSchema.php b/src/StoreApi/Schemas/V1/ShippingAddressSchema.php index 4f7b93d0331..c56c421f652 100644 --- a/src/StoreApi/Schemas/V1/ShippingAddressSchema.php +++ b/src/StoreApi/Schemas/V1/ShippingAddressSchema.php @@ -42,21 +42,34 @@ public function get_item_response( $address ) { $shipping_state = ''; } + // @TODO: add additional fields to the response. + if ( $address instanceof \WC_Order ) { + // get additional fields from order. + $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_order( $address ); + } elseif ( $address instanceof \WC_Customer ) { + // get additional fields from customer. + $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_customer( $address ); + } + return $this->prepare_html_response( - [ - 'first_name' => $address->get_shipping_first_name(), - 'last_name' => $address->get_shipping_last_name(), - 'company' => $address->get_shipping_company(), - 'address_1' => $address->get_shipping_address_1(), - 'address_2' => $address->get_shipping_address_2(), - 'city' => $address->get_shipping_city(), - 'state' => $shipping_state, - 'postcode' => $address->get_shipping_postcode(), - 'country' => $shipping_country, - 'phone' => $address->get_shipping_phone(), - ] + array_merge( + [ + 'first_name' => $address->get_shipping_first_name(), + 'last_name' => $address->get_shipping_last_name(), + 'company' => $address->get_shipping_company(), + 'address_1' => $address->get_shipping_address_1(), + 'address_2' => $address->get_shipping_address_2(), + 'city' => $address->get_shipping_city(), + 'state' => $shipping_state, + 'postcode' => $address->get_shipping_postcode(), + 'country' => $shipping_country, + 'phone' => $address->get_shipping_phone(), + ], + $additional_address_fields + ) ); } + throw new RouteException( 'invalid_object_type', sprintf( diff --git a/src/StoreApi/StoreApi.php b/src/StoreApi/StoreApi.php index 7b740e37a2e..ede88a1d126 100644 --- a/src/StoreApi/StoreApi.php +++ b/src/StoreApi/StoreApi.php @@ -1,6 +1,7 @@ register( + CheckoutFields::class, + function () use ( $checkout_fields ) { + return $checkout_fields; + } + ); + add_action( 'rest_api_init', function() { diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php index 38a41e97b98..9bc4bf015f8 100644 --- a/src/StoreApi/Utilities/CheckoutTrait.php +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -1,6 +1,7 @@ order->set_payment_method( $this->get_request_payment_method_id( $request ) ); $this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) ); + $this->persist_additional_fields_for_order( $request ); + wc_do_deprecated_action( '__experimental_woocommerce_blocks_checkout_update_order_from_request', array( @@ -180,4 +191,31 @@ private function get_request_payment_method_title( \WP_REST_Request $request ) { $payment_method = $this->get_request_payment_method( $request ); return is_null( $payment_method ) ? '' : $payment_method->get_title(); } + + /** + * Persist additional fields for the order after validating them. + * + * @param \WP_REST_Request $request Full details about the request. + * + * @throws RouteException On error. + */ + private function persist_additional_fields_for_order( \WP_REST_Request $request ) { + // @TODO: finish this function to actually throw errors. + $errors = new \WP_Error(); + $request_fields = $request['additional_fields'] ?? []; + foreach ( $request_fields as $key => $value ) { + try { + $this->additional_fields_controller->validate_field_for_group( $key, $value, 'additional' ); + } catch ( \Exception $e ) { + $errors[] = $e->getMessage(); + continue; + } + $this->additional_fields_controller->persist_field( $key, $value, $this->order ); + } + + if ( $errors->has_errors() ) { + throw new RouteException( 'woocommerce_rest_checkout_invalid_additional_fields', $errors->get_error_messages(), 400 ); + } + + } } diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index 018e7b05669..eff448c697f 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -10,6 +10,13 @@ */ class OrderController { + /** + * Checkout fields controller. + * + * @var CheckoutFields + */ + private $additional_fields_controller; + /** * Create order and set props based on global settings. * @@ -132,7 +139,12 @@ public function sync_customer_data_with_order( \WC_Order $order ) { 'shipping_phone' => $order->get_shipping_phone(), ] ); + $order_fields = $this->additional_fields_controller->get_all_fields_from_order( $order ); + $customer_fields = $this->additional_fields_controller->filter_fields_for_customer( $order_fields ); + foreach ( $customer_fields as $key => $value ) { + $this->additional_fields_controller->persist_field_for_customer( $key, $value, $customer ); + } $customer->save(); }; } @@ -274,16 +286,16 @@ protected function validate_email( \WC_Order $order ) { protected function validate_addresses( \WC_Order $order ) { $errors = new \WP_Error(); $needs_shipping = wc()->cart->needs_shipping(); - $billing_address = $order->get_address( 'billing' ); - $shipping_address = $order->get_address( 'shipping' ); + $billing_country = $order->get_billing_country(); + $shipping_country = $order->get_shipping_country(); - if ( $needs_shipping && ! $this->validate_allowed_country( $shipping_address['country'], (array) wc()->countries->get_shipping_countries() ) ) { + if ( $needs_shipping && ! $this->validate_allowed_country( $shipping_country, (array) wc()->countries->get_shipping_countries() ) ) { throw new RouteException( 'woocommerce_rest_invalid_address_country', sprintf( /* translators: %s country code. */ __( 'Sorry, we do not ship orders to the provided country (%s)', 'woo-gutenberg-products-block' ), - $shipping_address['country'] + $shipping_country ), 400, [ @@ -292,13 +304,13 @@ protected function validate_addresses( \WC_Order $order ) { ); } - if ( ! $this->validate_allowed_country( $billing_address['country'], (array) wc()->countries->get_allowed_countries() ) ) { + if ( ! $this->validate_allowed_country( $billing_country, (array) wc()->countries->get_allowed_countries() ) ) { throw new RouteException( 'woocommerce_rest_invalid_address_country', sprintf( /* translators: %s country code. */ __( 'Sorry, we do not allow orders from the provided country (%s)', 'woo-gutenberg-products-block' ), - $billing_address['country'] + $billing_country ), 400, [ @@ -308,9 +320,9 @@ protected function validate_addresses( \WC_Order $order ) { } if ( $needs_shipping ) { - $this->validate_address_fields( $shipping_address, 'shipping', $errors ); + $this->validate_address_fields( $order, 'shipping', $errors ); } - $this->validate_address_fields( $billing_address, 'billing', $errors ); + $this->validate_address_fields( $order, 'billing', $errors ); if ( ! $errors->has_errors() ) { return; @@ -353,56 +365,21 @@ protected function validate_allowed_country( $country, array $allowed_countries /** * Check all required address fields are set and return errors if not. * - * @param array $address Address array. + * @param \WC_Order $order Order object. * @param string $address_type billing or shipping address, used in error messages. * @param \WP_Error $errors Error object. */ - protected function validate_address_fields( $address, $address_type, \WP_Error $errors ) { + protected function validate_address_fields( \WC_Order $order, $address_type, \WP_Error $errors ) { + // Ideally get_country_locale would already include additional fields. $all_locales = wc()->countries->get_country_locale(); + $address = $order->get_address( $address_type ); $current_locale = isset( $all_locales[ $address['country'] ] ) ? $all_locales[ $address['country'] ] : []; /** * We are not using wc()->counties->get_default_address_fields() here because that is filtered. Instead, this array * is based on assets/js/base/components/cart-checkout/address-form/default-address-fields.js */ - $address_fields = [ - 'first_name' => [ - 'label' => __( 'First name', 'woo-gutenberg-products-block' ), - 'required' => true, - ], - 'last_name' => [ - 'label' => __( 'Last name', 'woo-gutenberg-products-block' ), - 'required' => true, - ], - 'company' => [ - 'label' => __( 'Company', 'woo-gutenberg-products-block' ), - 'required' => false, - ], - 'address_1' => [ - 'label' => __( 'Address', 'woo-gutenberg-products-block' ), - 'required' => true, - ], - 'address_2' => [ - 'label' => __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ), - 'required' => false, - ], - 'country' => [ - 'label' => __( 'Country/Region', 'woo-gutenberg-products-block' ), - 'required' => true, - ], - 'city' => [ - 'label' => __( 'City', 'woo-gutenberg-products-block' ), - 'required' => true, - ], - 'state' => [ - 'label' => __( 'State/County', 'woo-gutenberg-products-block' ), - 'required' => true, - ], - 'postcode' => [ - 'label' => __( 'Postal code', 'woo-gutenberg-products-block' ), - 'required' => true, - ], - ]; + $address_fields = $this->additional_fields_controller->get_fields_for_group( 'address' ); if ( $current_locale ) { foreach ( $current_locale as $key => $field ) { @@ -738,5 +715,9 @@ protected function update_addresses_from_cart( \WC_Order $order ) { 'shipping_phone' => wc()->customer->get_shipping_phone(), ] ); + $customer_fields = $this->additional_fields_controller->get_all_fields_for_customer( wc()->customer ); + foreach ( $customer_fields as $key => $value ) { + $this->additional_fields_controller->persist_field_for_order( $key, $value, $order ); + } } } From 6f90954e807b7862739f1e9703d15383c87aac67 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 5 Dec 2023 17:57:13 +0100 Subject: [PATCH 04/46] add data reading and saving functions --- src/Domain/Services/CheckoutFields.php | 166 +++++++++++++++++++-- src/StoreApi/Utilities/OrderController.php | 2 +- 2 files changed, 153 insertions(+), 15 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 3f9e2adbb9f..86fe320c1af 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; +use WC_Customer; /** * Service class managing checkout fields and its related extensibility points. @@ -38,6 +39,27 @@ abstract class CheckoutFields { */ private $asset_data_registry; + /** + * Billing fields meta key. + * + * @var string + */ + const BILLING_FIELDS_KEY = '_additional_billing_fields'; + + /** + * Shipping fields meta key. + * + * @var string + */ + const SHIPPING_FIELDS_KEY = '_additional_shipping_fields'; + + /** + * Additional fields meta key. + * + * @var string + */ + const ADDITIONAL_FIELDS_KEY = '_additional_fields'; + /** * Sets up core fields. * @@ -243,11 +265,11 @@ abstract public function register_checkout_field( $options ); /** * Returns an array of fields for a given group. * - * @param string $group The group to get fields for (address|contact|additional). + * @param string $location The location to get fields for (address|contact|additional). * * @return array An array of fields. */ - abstract public function get_fields_for_group( $group ); + abstract public function get_fields_for_location( $location ); /** * Returns an array of fields keys for a the address group. @@ -275,13 +297,13 @@ abstract protected function get_additional_fields_keys(); * * @param string $key The field key. * @param mixed $value The field value. - * @param string $group The group to validate the field for (address|contact|additional). + * @param string $location The location to validate the field for (address|contact|additional). * - * TODO: we might not need the group param here. + * TODO: we might not need the location param here. * * @return true|\WP_Error True if the field is valid, a WP_Error otherwise. */ - abstract public function validate_field_for_group( $key, $value, $group ); + abstract public function validate_field_for_location( $key, $value, $location ); /** * Returns true if the given key is a valid field. @@ -290,7 +312,9 @@ abstract public function validate_field_for_group( $key, $value, $group ); * * @return bool True if the field is valid, false otherwise. */ - abstract public function is_field( $key ); + public function is_field( $key ) { + return array_key_exists( $key, $this->additional_fields ); + } /** * Persists a field value for a given order. This would also optionally set the field value on the customer. @@ -298,10 +322,22 @@ abstract public function is_field( $key ); * @param string $key The field key. * @param mixed $value The field value. * @param \WC_Order $order The order to persist the field for. + * @param bool $set_customer Whether to set the field value on the customer or not. * * @return void */ - abstract public function persist_field( $key, $value, $order ); + public function persist_field( $key, $value, $order, $set_customer = true ) { + $this->set_array_meta( $key, $value, $order ); + if ( $set_customer ) { + if ( isset( wc()->customer ) ) { + $this->set_array_meta( $key, $value, wc()->customer ); + } elseif ( $order->get_customer_id() ) { + $customer = new \WC_Customer( $order->get_customer_id() ); + $this->set_array_meta( $key, $value, $customer ); + $customer->save(); + } + } + } /** * Persists a field value for a given customer. @@ -312,26 +348,35 @@ abstract public function persist_field( $key, $value, $order ); * * @return void */ - abstract public function persist_field_for_customer( $key, $value, $customer ); + public function persist_field_for_customer( $key, $value, $customer ) { + $this->set_array_meta( $key, $value, $customer ); + } + /** - * Returns a field value for a given customer. + * Returns a field value for a given object. * - * @param string $field The field key. + * @param string $key The field key. * @param \WC_Customer $customer The customer to get the field value for. + * @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group. * * @return mixed The field value. */ - abstract public function get_field_from_customer( $field, $customer ); + public function get_field_from_customer( $key, $customer, $group = '' ) { + return $this->get_field_from_object( $key, $customer, $group ); + } /** * Returns a field value for a given order. * * @param string $field The field key. * @param \WC_Order $order The order to get the field value for. + * @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group. * * @return mixed The field value. */ - abstract public function get_field_from_order( $field, $order ); + public function get_field_from_order( $field, $order, $group ) { + return $this->get_field_from_object( $field, $order, $group ); + } /** * Returns an array of all fields values for a given customer. @@ -340,7 +385,9 @@ abstract public function get_field_from_order( $field, $order ); * * @return array An array of fields. */ - abstract public function get_all_fields_from_customer( $customer ); + public function get_all_fields_from_customer( $customer ) { + return $this->get_all_fields_from_object( $customer ); + } /** * Returns an array of all fields values for a given order. @@ -349,6 +396,97 @@ abstract public function get_all_fields_from_customer( $customer ); * * @return array An array of fields. */ - abstract public function get_all_fields_from_order( $order ); + public function get_all_fields_from_order( $order ) { + return $this->get_all_fields_from_object( $order ); + } + + /** + * Sets a field value in an array meta, supporting routing things to billing, shipping, or additional fields, based on a prefix for the key. + * + * @param string $key The field key. + * @param mixed $value The field value. + * @param \WC_Customer|\WC_Order $object The object to set the field value for. + * + * @return void + */ + private function set_array_meta( $key, $value, $object ) { + $meta_key = ''; + if ( 0 === strpos( $key, '/billing/' ) ) { + $meta_key = self::BILLING_FIELDS_KEY; + $key = str_replace( '/billing/', '', $key ); + } elseif ( 0 === strpos( $key, '/shipping/' ) ) { + $meta_key = self::SHIPPING_FIELDS_KEY; + $key = str_replace( '/shipping/', '', $key ); + } else { + $meta_key = self::ADDITIONAL_FIELDS_KEY; + } + + $meta_data = $object->get_meta( $meta_key, true ); + if ( ! is_array( $meta_data ) ) { + $meta_data = array(); + } + $meta_data[ $key ] = $value; + $object->update_meta_data( $meta_key, $meta_data ); + } + + /** + * Returns a field value for a given object. + * + * @param string $key The field key. + * @param \WC_Customer|\WC_Order $object The customer to get the field value for. + * @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group. + * + * @return mixed The field value. + */ + private function get_field_from_object( $key, $object, $group = '' ) { + $meta_key = ''; + if ( 0 === strpos( $key, '/billing/' ) || 'billing' === $group ) { + $meta_key = self::BILLING_FIELDS_KEY; + $key = str_replace( '/billing/', '', $key ); + } elseif ( 0 === strpos( $key, '/shipping/' ) || 'shipping' === $group ) { + $meta_key = self::SHIPPING_FIELDS_KEY; + $key = str_replace( '/shipping/', '', $key ); + } else { + $meta_key = self::ADDITIONAL_FIELDS_KEY; + } + + $meta_data = $object->get_meta( $meta_key, true ); + + if ( ! is_array( $meta_data ) ) { + return ''; + } + + if ( ! isset( $meta_data[ $key ] ) ) { + return ''; + } + + return $meta_data[ $key ]; + } + + /** + * Returns an array of all fields values for a given object. It would add the billing or shipping prefix to the keys. + * + * @param \WC_Order|\WC_Customer $object The object to get the fields for. + * + * @return array An array of fields. + */ + private function get_all_fields_from_object( $object ) { + $billing_fields = $object->get_meta( self::BILLING_FIELDS_KEY, true ); + $shipping_fields = $object->get_meta( self::SHIPPING_FIELDS_KEY, true ); + $additional_fields = $object->get_meta( self::ADDITIONAL_FIELDS_KEY, true ); + + $fields = array(); + + foreach ( $billing_fields as $key => $value ) { + $fields[ '/billing/' . $key ] = $value; + } + + foreach ( $shipping_fields as $key => $value ) { + $fields[ '/shipping/' . $key ] = $value; + } + + return array_merge( $fields, $additional_fields ); + } + } diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index eff448c697f..45605671a31 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -717,7 +717,7 @@ protected function update_addresses_from_cart( \WC_Order $order ) { ); $customer_fields = $this->additional_fields_controller->get_all_fields_for_customer( wc()->customer ); foreach ( $customer_fields as $key => $value ) { - $this->additional_fields_controller->persist_field_for_order( $key, $value, $order ); + $this->additional_fields_controller->persist_field( $key, $value, $order, false ); } } } From 48ee2ddf7c1361e43e41dc56ed25a7337ee4541b Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Tue, 5 Dec 2023 17:41:05 +0000 Subject: [PATCH 05/46] Add woocommerce_blocks_register_checkout_field function --- src/Domain/Services/functions.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/Domain/Services/functions.php diff --git a/src/Domain/Services/functions.php b/src/Domain/Services/functions.php new file mode 100644 index 00000000000..b05baa5c50e --- /dev/null +++ b/src/Domain/Services/functions.php @@ -0,0 +1,20 @@ +get( \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields::class ); + $result = $checkout_fields->register_checkout_field( $options ); + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + } +} From 88ee619ac9220920878738b693528af648e8c93d Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Tue, 5 Dec 2023 17:41:41 +0000 Subject: [PATCH 06/46] Add example code for registering a field --- src/Domain/Services/functions.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Domain/Services/functions.php b/src/Domain/Services/functions.php index b05baa5c50e..2d3b4c1388b 100644 --- a/src/Domain/Services/functions.php +++ b/src/Domain/Services/functions.php @@ -18,3 +18,19 @@ function woocommerce_blocks_register_checkout_field( $options ) { } } } + +/** + * Example code to register a checkout field. + */ +woocommerce_blocks_register_checkout_field( + array( + 'id' => 'plugin/dialling-code', + 'label' => __( 'Dialling code', 'woo-gutenberg-products-block' ), + 'optionalLabel' => __( 'Dialling code (optional)', 'woo-gutenberg-products-block' ), + 'required' => false, + 'hidden' => true, + 'autocomplete' => 'dialling-code', + 'autocapitalize' => 'characters', + 'location' => 'address', + ) +); From 95cf8953bea5205118969e45a551bfd450ba50a9 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Tue, 5 Dec 2023 17:42:33 +0000 Subject: [PATCH 07/46] Create register_checkout_field function --- src/Domain/Services/CheckoutFields.php | 39 ++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 86fe320c1af..6cf7783bf3e 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -258,9 +258,44 @@ public function add_fields_data() { * * @param array $options The field options. * - * @return true|WP_Error True if the field was registered, a WP_Error otherwise. + * @return \WP_Error|void True if the field was registered, a WP_Error otherwise. */ - abstract public function register_checkout_field( $options ); + public function register_checkout_field( $options ) { + if ( empty( $options['id'] ) ) { + return new \WP_Error( 'woocommerce_blocks_checkout_field_id_required', __( 'The field id is required.', 'woo-gutenberg-products-block' ) ); + } + + if ( empty( $options['label'] ) ) { + return new \WP_Error( 'woocommerce_blocks_checkout_field_label_required', __( 'The field label is required.', 'woo-gutenberg-products-block' ) ); + } + + if ( empty( $options['location'] ) ) { + return new \WP_Error( 'woocommerce_blocks_checkout_field_location_required', __( 'The field location is required.', 'woo-gutenberg-products-block' ) ); + } + + if ( ! in_array( $options['location'], array_keys( $this->fields_locations ), true ) ) { + return new \WP_Error( 'woocommerce_blocks_checkout_field_location_invalid', __( 'The field location is invalid.', 'woo-gutenberg-products-block' ) ); + } + + // At this point, the essentials fields and its location should be set. + $location = $options['location']; + $id = $options['id']; + + // Check to see if field is already in the array. + if ( ! empty( $this->additional_fields[ $location ][ $id ] ) ) { + return new \WP_Error( 'woocommerce_blocks_checkout_field_already_registered', __( 'The field is already registered.', 'woo-gutenberg-products-block' ) ); + } + + // Insert new field into the correct location array. + $this->additional_fields[ $location ][ $id ] = array( + 'label' => $options['label'], + 'optionalLabel' => ! empty( $options['optionalLabel'] ) ? $options['optionalLabel'] : '', + 'required' => ! empty( $options['required'] ) ? $options['required'] : false, + 'hidden' => ! empty( $options['hidden'] ) ? $options['hidden'] : false, + 'autocomplete' => ! empty( $options['autocomplete'] ) ? $options['autocomplete'] : '', + 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', + ); + } /** * Returns an array of fields for a given group. From 164f4690b45160158370a6f9259b5e2ff613a1c4 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 5 Dec 2023 19:30:17 +0100 Subject: [PATCH 08/46] finish all code handling --- src/Domain/Services/CheckoutFields.php | 37 ++++++- src/StoreApi/Routes/V1/CartUpdateCustomer.php | 103 +++++++++++++----- src/StoreApi/Routes/V1/Checkout.php | 8 +- src/StoreApi/Routes/V1/CheckoutOrder.php | 2 +- .../Schemas/V1/AbstractAddressSchema.php | 64 +++++++++-- .../Schemas/V1/BillingAddressSchema.php | 14 ++- src/StoreApi/Schemas/V1/CheckoutSchema.php | 50 ++++++++- .../Schemas/V1/ShippingAddressSchema.php | 14 ++- src/StoreApi/Utilities/CheckoutTrait.php | 6 +- src/StoreApi/Utilities/OrderController.php | 7 +- 10 files changed, 247 insertions(+), 58 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 6cf7783bf3e..2909d4c0e34 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -249,7 +249,7 @@ public function init() { * Add fields data to the asset data registry. */ public function add_fields_data() { - $this->asset_data_registry->add( 'defaultAddressFields', array_merge( $this->core_fields, $this->additional_fields ), true ); + $this->asset_data_registry->add( 'defaultAddressFields', $this->get_fields(), true ); $this->asset_data_registry->add( 'addressFieldsLocations', $this->fields_locations, true ); } @@ -297,6 +297,15 @@ public function register_checkout_field( $options ) { ); } + /** + * Returns an array of all fields. + * + * @return array An array of fields. + */ + public function get_fields() { + return array_merge( $this->core_fields, $this->additional_fields ); + } + /** * Returns an array of fields for a given group. * @@ -311,21 +320,21 @@ abstract public function get_fields_for_location( $location ); * * @return array An array of fields keys. */ - abstract protected function get_address_fields_keys(); + abstract public function get_address_fields_keys(); /** * Returns an array of fields keys for a the contact group. * * @return array An array of fields keys. */ - abstract protected function get_contact_fields_keys(); + abstract public function get_contact_fields_keys(); /** * Returns an array of fields keys for a the additional area group. * * @return array An array of fields keys. */ - abstract protected function get_additional_fields_keys(); + abstract public function get_additional_fields_keys(); /** * Validates a field value for a given group. @@ -409,7 +418,7 @@ public function get_field_from_customer( $key, $customer, $group = '' ) { * * @return mixed The field value. */ - public function get_field_from_order( $field, $order, $group ) { + public function get_field_from_order( $field, $order, $group = '' ) { return $this->get_field_from_object( $field, $order, $group ); } @@ -523,5 +532,23 @@ private function get_all_fields_from_object( $object ) { return array_merge( $fields, $additional_fields ); } + /** + * From a set of fields, returns only the ones that should be saved to the customer. + * For now, this only supports fields in address location. + * + * @param array $fields The fields to filter. + * + * @return array The filtered fields. + */ + public function filter_fields_for_customer( $fields ) { + $customer_fields_keys = $this->get_address_fields_keys(); + return array_filter( + $fields, + function( $key ) use ( $customer_fields_keys ) { + return in_array( $key, $customer_fields_keys, true ); + }, + ARRAY_FILTER_USE_KEY + ); + } } diff --git a/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/src/StoreApi/Routes/V1/CartUpdateCustomer.php index df9363e1412..2ec9f0bc4a0 100644 --- a/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -182,7 +182,19 @@ protected function get_route_post_response( \WP_REST_Request $request ) { 'shipping_phone' => $shipping['phone'] ?? null, ) ); - // @TODO: add additional fields here like we did with order controller. + // We want to only get additional fields passed, since core ones are already saved. + $core_fields = array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'phone', 'email' ); + + $additional_shipping_values = array_diff_key( $shipping, array_flip( $core_fields ) ); + $additional_billing_values = array_diff_key( $billing, array_flip( $core_fields ) ); + + // We save them one by one, and we add the group prefix. + foreach ( $additional_shipping_values as $key => $value ) { + $this->additional_fields_controller->persist_field_for_customer( "/shipping/{$key}", $value, $customer ); + } + foreach ( $additional_billing_values as $key => $value ) { + $this->additional_fields_controller->persist_field_for_customer( "/billing/{$key}", $value, $customer ); + } wc_do_deprecated_action( 'woocommerce_blocks_cart_update_customer_from_request', @@ -223,6 +235,21 @@ protected function get_customer_billing_address( \WC_Customer $customer ) { $billing_country = $customer->get_billing_country(); $billing_state = $customer->get_billing_state(); + $additional_fields = $this->additional_fields_controller->get_all_fields_from_customer( $customer ); + + $additional_fields = array_reduce( + array_keys( $additional_fields ), + function( $carry, $key, $value ) use ( $additional_fields ) { + if ( 0 === strpos( $key, '/billing/' ) ) { + $value = $additional_fields[ $key ]; + $key = str_replace( '/billing/', '', $key ); + $carry[ $key ] = $value; + } + return $carry; + }, + array() + ); + /** * There's a bug in WooCommerce core in which not having a state ("") would result in us validating against the store's state. * This resets the state to an empty string if it doesn't match the country. @@ -232,21 +259,22 @@ protected function get_customer_billing_address( \WC_Customer $customer ) { if ( ! $validation_util->validate_state( $billing_state, $billing_country ) ) { $billing_state = ''; } - return [ - 'first_name' => $customer->get_billing_first_name(), - 'last_name' => $customer->get_billing_last_name(), - 'company' => $customer->get_billing_company(), - 'address_1' => $customer->get_billing_address_1(), - 'address_2' => $customer->get_billing_address_2(), - 'city' => $customer->get_billing_city(), - 'state' => $billing_state, - 'postcode' => $customer->get_billing_postcode(), - 'country' => $billing_country, - 'phone' => $customer->get_billing_phone(), - 'email' => $customer->get_billing_email(), - ]; - // @TODO: also include additional address fields for billing. - // get_all_fields_from_customer( $customer, 'billing' ), + return array_merge( + [ + 'first_name' => $customer->get_billing_first_name(), + 'last_name' => $customer->get_billing_last_name(), + 'company' => $customer->get_billing_company(), + 'address_1' => $customer->get_billing_address_1(), + 'address_2' => $customer->get_billing_address_2(), + 'city' => $customer->get_billing_city(), + 'state' => $billing_state, + 'postcode' => $customer->get_billing_postcode(), + 'country' => $billing_country, + 'phone' => $customer->get_billing_phone(), + 'email' => $customer->get_billing_email(), + ], + $additional_fields + ); } /** @@ -256,19 +284,34 @@ protected function get_customer_billing_address( \WC_Customer $customer ) { * @return array */ protected function get_customer_shipping_address( \WC_Customer $customer ) { - return [ - 'first_name' => $customer->get_shipping_first_name(), - 'last_name' => $customer->get_shipping_last_name(), - 'company' => $customer->get_shipping_company(), - 'address_1' => $customer->get_shipping_address_1(), - 'address_2' => $customer->get_shipping_address_2(), - 'city' => $customer->get_shipping_city(), - 'state' => $customer->get_shipping_state(), - 'postcode' => $customer->get_shipping_postcode(), - 'country' => $customer->get_shipping_country(), - 'phone' => $customer->get_shipping_phone(), - ]; - // @TODO: also include additional address fields for shipping. - // get_all_fields_from_customer( $customer, 'shipping' ), + $additional_fields = $this->additional_fields_controller->get_all_fields_from_customer( $customer ); + + $additional_fields = array_reduce( + array_keys( $additional_fields ), + function( $carry, $key, $value ) use ( $additional_fields ) { + if ( 0 === strpos( $key, '/shipping/' ) ) { + $value = $additional_fields[ $key ]; + $key = str_replace( '/shipping/', '', $key ); + $carry[ $key ] = $value; + } + return $carry; + }, + array() + ); + return array_merge( + [ + 'first_name' => $customer->get_shipping_first_name(), + 'last_name' => $customer->get_shipping_last_name(), + 'company' => $customer->get_shipping_company(), + 'address_1' => $customer->get_shipping_address_1(), + 'address_2' => $customer->get_shipping_address_2(), + 'city' => $customer->get_shipping_city(), + 'state' => $customer->get_shipping_state(), + 'postcode' => $customer->get_shipping_postcode(), + 'country' => $customer->get_shipping_country(), + 'phone' => $customer->get_shipping_phone(), + ], + $additional_fields + ); } } diff --git a/src/StoreApi/Routes/V1/Checkout.php b/src/StoreApi/Routes/V1/Checkout.php index 9a525969ede..2f93309be30 100644 --- a/src/StoreApi/Routes/V1/Checkout.php +++ b/src/StoreApi/Routes/V1/Checkout.php @@ -1,6 +1,7 @@ schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) ), ], diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index a3914484de9..c411a19e826 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -7,7 +7,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait; use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait; -// @TODO: add custom fields support. +// Custom Fields Note: This doesn't support custom fields fully yet. /** * CheckoutOrder class. */ diff --git a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index 352969169d9..47cc8fa991b 100644 --- a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -1,6 +1,7 @@ get_properties() ), '' ), (array) $address ); + $address = array_reduce( + array_keys( $address ), + function( $carry, $key ) use ( $address, $validation_util ) { + if ( 'country' === $key ) { + $carry[ $key ] = wc_strtoupper( sanitize_text_field( wp_unslash( $address[ $key ] ) ) ); + } elseif ( 'state' === $key ) { + $carry[ $key ] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address[ $key ] ) ), $address['country'] ); + } elseif ( 'postcode' === $key ) { + $carry[ $key ] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; + } else { + $carry[ $key ] = sanitize_text_field( wp_unslash( $address[ $key ] ) ); + } + return $carry; + }, + [] + ); - $address = array_merge( array_fill_keys( array_keys( $this->get_properties() ), '' ), (array) $address ); - $address['country'] = wc_strtoupper( sanitize_text_field( wp_unslash( $address['country'] ) ) ); - $address['first_name'] = sanitize_text_field( wp_unslash( $address['first_name'] ) ); - $address['last_name'] = sanitize_text_field( wp_unslash( $address['last_name'] ) ); - $address['company'] = sanitize_text_field( wp_unslash( $address['company'] ) ); - $address['address_1'] = sanitize_text_field( wp_unslash( $address['address_1'] ) ); - $address['address_2'] = sanitize_text_field( wp_unslash( $address['address_2'] ) ); - $address['city'] = sanitize_text_field( wp_unslash( $address['city'] ) ); - $address['state'] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address['state'] ) ), $address['country'] ); - $address['postcode'] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; - $address['phone'] = sanitize_text_field( wp_unslash( $address['phone'] ) ); - // @TODO: sanitize additional address fields. return $address; } @@ -164,4 +179,29 @@ public function validate_callback( $address, $request, $param ) { return $errors->has_errors( $errors ) ? $errors : true; } + + /** + * Get additional address fields schema. + * + * @return array + */ + protected function get_additional_address_fields_schema() { + $additional_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); + $additional_fields = array_filter( + $this->additional_fields_controller->get_fields(), + function( $field ) use ( $additional_fields_keys ) { + return in_array( $field['key'], $additional_fields_keys, true ); + } + ); + $schema = []; + foreach ( $additional_fields as $key => $field ) { + $schema[ $key ] = [ + 'description' => $field['label'], + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ]; + } + return $schema; + } } diff --git a/src/StoreApi/Schemas/V1/BillingAddressSchema.php b/src/StoreApi/Schemas/V1/BillingAddressSchema.php index b13df79a18e..68fb3b8bff6 100644 --- a/src/StoreApi/Schemas/V1/BillingAddressSchema.php +++ b/src/StoreApi/Schemas/V1/BillingAddressSchema.php @@ -99,7 +99,6 @@ public function get_item_response( $address ) { $billing_state = ''; } - // @TODO: add additional fields to the response. if ( $address instanceof \WC_Order ) { // get additional fields from order. $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_order( $address ); @@ -108,6 +107,19 @@ public function get_item_response( $address ) { $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_customer( $address ); } + $additional_address_fields = array_reduce( + array_keys( $additional_address_fields ), + function( $carry, $key ) use ( $additional_address_fields ) { + if ( 0 === strpos( $key, '/billing/' ) ) { + $value = $additional_address_fields[ $key ]; + $key = str_replace( '/billing/', '', $key ); + $carry[ $key ] = $value; + } + return $carry; + }, + [] + ); + return $this->prepare_html_response( \array_merge( [ diff --git a/src/StoreApi/Schemas/V1/CheckoutSchema.php b/src/StoreApi/Schemas/V1/CheckoutSchema.php index 9e5b5e105e8..ed805a8c537 100644 --- a/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -4,7 +4,7 @@ use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; - +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; /** * CheckoutSchema class. @@ -45,6 +45,13 @@ class CheckoutSchema extends AbstractSchema { */ protected $image_attachment_schema; + /** + * Additional fields controller instance. + * + * @var CheckoutFields + */ + private CheckoutFields $additional_fields_controller; + /** * Constructor. * @@ -237,4 +244,45 @@ function( $key, $value ) { $payment_details ); } + + /** + * Get the additional fields response. + * + * @param \WC_Order $order Order object. + * @return array + */ + protected function get_additional_fields_response( \WC_Order $order ) { + $additional_fields_keys = array_merge( $this->additional_fields_controller->get_contact_fields_keys(), $this->additional_fields_controller->get_additional_fields_keys() ); + + $response = []; + foreach ( $additional_fields_keys as $key ) { + $response[ $key ] = $this->additional_fields_controller->get_field_from_order( $key, $order ); + } + return $response; + } + + /** + * Get the schema for additional fields. + * + * @return array + */ + protected function get_additional_fields_schema() { + $additional_fields_keys = array_merge( $this->additional_fields_controller->get_contact_fields_keys(), $this->additional_fields_controller->get_additional_fields_keys() ); + $additional_fields = array_filter( + $this->additional_fields_controller->get_fields(), + function( $field ) use ( $additional_fields_keys ) { + return in_array( $field['key'], $additional_fields_keys, true ); + } + ); + $schema = []; + foreach ( $additional_fields as $key => $field ) { + $schema[ $key ] = [ + 'description' => $field['label'], + 'type' => 'string', + 'context' => [ 'view', 'edit' ], + 'required' => true, + ]; + } + return $schema; + } } diff --git a/src/StoreApi/Schemas/V1/ShippingAddressSchema.php b/src/StoreApi/Schemas/V1/ShippingAddressSchema.php index c56c421f652..1461d3e0fad 100644 --- a/src/StoreApi/Schemas/V1/ShippingAddressSchema.php +++ b/src/StoreApi/Schemas/V1/ShippingAddressSchema.php @@ -42,7 +42,6 @@ public function get_item_response( $address ) { $shipping_state = ''; } - // @TODO: add additional fields to the response. if ( $address instanceof \WC_Order ) { // get additional fields from order. $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_order( $address ); @@ -51,6 +50,19 @@ public function get_item_response( $address ) { $additional_address_fields = $this->additional_fields_controller->get_all_fields_from_customer( $address ); } + $additional_address_fields = array_reduce( + array_keys( $additional_address_fields ), + function( $carry, $key ) use ( $additional_address_fields ) { + if ( 0 === strpos( $key, '/shipping/' ) ) { + $value = $additional_address_fields[ $key ]; + $key = str_replace( '/shipping/', '', $key ); + $carry[ $key ] = $value; + } + return $carry; + }, + [] + ); + return $this->prepare_html_response( array_merge( [ diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php index 9bc4bf015f8..0c2bf825d84 100644 --- a/src/StoreApi/Utilities/CheckoutTrait.php +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -18,7 +18,7 @@ trait CheckoutTrait { * * @var CheckoutFields */ - private $additional_fields_controller; + private CheckoutFields $additional_fields_controller; /** * Prepare a single item for response. Handles setting the status based on the payment result. @@ -205,12 +205,12 @@ private function persist_additional_fields_for_order( \WP_REST_Request $request $request_fields = $request['additional_fields'] ?? []; foreach ( $request_fields as $key => $value ) { try { - $this->additional_fields_controller->validate_field_for_group( $key, $value, 'additional' ); + $this->additional_fields_controller->validate_field_for_location( $key, $value, 'additional' ); } catch ( \Exception $e ) { $errors[] = $e->getMessage(); continue; } - $this->additional_fields_controller->persist_field( $key, $value, $this->order ); + $this->additional_fields_controller->persist_field( $key, $value, $this->order, true ); } if ( $errors->has_errors() ) { diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index 45605671a31..a43060e3d9a 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -3,6 +3,7 @@ use \Exception; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; /** * OrderController class. @@ -15,7 +16,7 @@ class OrderController { * * @var CheckoutFields */ - private $additional_fields_controller; + private CheckoutFields $additional_fields_controller; /** * Create order and set props based on global settings. @@ -379,7 +380,7 @@ protected function validate_address_fields( \WC_Order $order, $address_type, \WP * We are not using wc()->counties->get_default_address_fields() here because that is filtered. Instead, this array * is based on assets/js/base/components/cart-checkout/address-form/default-address-fields.js */ - $address_fields = $this->additional_fields_controller->get_fields_for_group( 'address' ); + $address_fields = $this->additional_fields_controller->get_fields_for_location( 'address' ); if ( $current_locale ) { foreach ( $current_locale as $key => $field ) { @@ -715,7 +716,7 @@ protected function update_addresses_from_cart( \WC_Order $order ) { 'shipping_phone' => wc()->customer->get_shipping_phone(), ] ); - $customer_fields = $this->additional_fields_controller->get_all_fields_for_customer( wc()->customer ); + $customer_fields = $this->additional_fields_controller->get_all_fields_from_customer( wc()->customer ); foreach ( $customer_fields as $key => $value ) { $this->additional_fields_controller->persist_field( $key, $value, $order, false ); } From c09894c2d3456cae75108c581c4b4b73103ba9d7 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 5 Dec 2023 20:09:35 +0100 Subject: [PATCH 09/46] fix all code errors --- phpcs.xml | 2 +- src/Domain/Services/CheckoutFields.php | 72 ++++++++++++++----- src/StoreApi/Routes/V1/AbstractCartRoute.php | 2 +- src/StoreApi/Routes/V1/AbstractRoute.php | 14 +++- src/StoreApi/Routes/V1/Checkout.php | 6 -- src/StoreApi/Routes/V1/Order.php | 2 +- .../Schemas/V1/AbstractAddressSchema.php | 26 ++++--- src/StoreApi/Schemas/V1/AbstractSchema.php | 13 +++- src/StoreApi/Schemas/V1/CheckoutSchema.php | 27 ++++--- src/StoreApi/Schemas/V1/OrderSchema.php | 2 +- src/StoreApi/Utilities/CheckoutTrait.php | 8 --- src/StoreApi/Utilities/OrderController.php | 19 ++++- 12 files changed, 125 insertions(+), 68 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index fd69c7936f6..51ada14dae7 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -13,7 +13,7 @@ (see https://github.com/woocommerce/woocommerce-blocks/blob/trunk/.github/release-initial-checklist.md#initial-preparation) --> - + diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 2909d4c0e34..11e1de756c2 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -8,7 +8,7 @@ /** * Service class managing checkout fields and its related extensibility points. */ -abstract class CheckoutFields { +class CheckoutFields { /** @@ -210,7 +210,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'woo-gutenberg-products-block' ), 'required' => false, - 'hidden' => true, + 'hidden' => false, 'autocomplete' => 'vat', 'autocapitalize' => 'characters', ), @@ -221,7 +221,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'woo-gutenberg-products-block' ), 'required' => false, - 'hidden' => true, + 'hidden' => false, 'autocomplete' => 'vat', 'autocapitalize' => 'characters', ), @@ -229,7 +229,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { $this->fields_locations = array( // omit email from shipping and billing fields. - 'address' => $this->get_address_fields_keys(), // everything here will be saved to customer and order. + 'address' => array_merge( \array_diff_key( array_keys( $this->core_fields ), array( 'email' ) ), array( 'plugin_vat' ) ), // everything here will be saved to customer and order. // @todo handle rendering contact fields. 'contact' => array( 'email' ), // everything here will be saved to order, and optionally to customer. // @todo handle rendering additional fields. @@ -282,12 +282,12 @@ public function register_checkout_field( $options ) { $id = $options['id']; // Check to see if field is already in the array. - if ( ! empty( $this->additional_fields[ $location ][ $id ] ) ) { + if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) { return new \WP_Error( 'woocommerce_blocks_checkout_field_already_registered', __( 'The field is already registered.', 'woo-gutenberg-products-block' ) ); } // Insert new field into the correct location array. - $this->additional_fields[ $location ][ $id ] = array( + $this->additional_fields[ $id ] = array( 'label' => $options['label'], 'optionalLabel' => ! empty( $options['optionalLabel'] ) ? $options['optionalLabel'] : '', 'required' => ! empty( $options['required'] ) ? $options['required'] : false, @@ -295,6 +295,8 @@ public function register_checkout_field( $options ) { 'autocomplete' => ! empty( $options['autocomplete'] ) ? $options['autocomplete'] : '', 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', ); + + array_push( $this->fields_locations[ $location ], $id ); } /** @@ -313,28 +315,38 @@ public function get_fields() { * * @return array An array of fields. */ - abstract public function get_fields_for_location( $location ); + public function get_fields_for_location( $location ) { + if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) { + return $this->fields_locations[ $location ]; + } + } /** * Returns an array of fields keys for a the address group. * * @return array An array of fields keys. */ - abstract public function get_address_fields_keys(); + public function get_address_fields_keys() { + return $this->fields_locations['address']; + } /** * Returns an array of fields keys for a the contact group. * * @return array An array of fields keys. */ - abstract public function get_contact_fields_keys(); + public function get_contact_fields_keys() { + return $this->fields_locations['contact']; + } /** * Returns an array of fields keys for a the additional area group. * * @return array An array of fields keys. */ - abstract public function get_additional_fields_keys(); + public function get_additional_fields_keys() { + return $this->fields_locations['additional']; + } /** * Validates a field value for a given group. @@ -347,7 +359,25 @@ abstract public function get_additional_fields_keys(); * * @return true|\WP_Error True if the field is valid, a WP_Error otherwise. */ - abstract public function validate_field_for_location( $key, $value, $location ); + public function validate_field_for_location( $key, $value, $location ) { + if ( ! $this->is_field( $key ) ) { + // translators: %s field key. + return new \WP_Error( 'woocommerce_blocks_checkout_field_invalid', \sprintf( __( 'The field %s is invalid.', 'woo-gutenberg-products-block' ), $key ) ); + } + + if ( ! in_array( $key, $this->fields_locations[ $location ], true ) ) { + // translators: %1$s field key, %2$s location. + return new \WP_Error( 'woocommerce_blocks_checkout_field_invalid_location', \sprintf( __( 'The field %1$s is invalid for the location %2$s.', 'woo-gutenberg-products-block' ), $key, $location ) ); + } + + $field = $this->additional_fields[ $key ]; + if ( ! empty( $field['required'] ) && empty( $value ) ) { + // translators: %s field key. + return new \WP_Error( 'woocommerce_blocks_checkout_field_required', \sprintf( __( 'The field %s is required.', 'woo-gutenberg-products-block' ), $key ) ); + } + + return true; + } /** * Returns true if the given key is a valid field. @@ -521,15 +551,25 @@ private function get_all_fields_from_object( $object ) { $fields = array(); - foreach ( $billing_fields as $key => $value ) { - $fields[ '/billing/' . $key ] = $value; + if ( is_array( $billing_fields ) ) { + foreach ( $billing_fields as $key => $value ) { + $fields[ '/billing/' . $key ] = $value; + } } - foreach ( $shipping_fields as $key => $value ) { - $fields[ '/shipping/' . $key ] = $value; + if ( is_array( $shipping_fields ) ) { + foreach ( $shipping_fields as $key => $value ) { + $fields[ '/shipping/' . $key ] = $value; + } + } + + if ( is_array( $additional_fields ) ) { + foreach ( $additional_fields as $key => $value ) { + $fields[ $key ] = $value; + } } - return array_merge( $fields, $additional_fields ); + return $fields; } /** diff --git a/src/StoreApi/Routes/V1/AbstractCartRoute.php b/src/StoreApi/Routes/V1/AbstractCartRoute.php index a60d4b87c1a..2f40c6603fd 100644 --- a/src/StoreApi/Routes/V1/AbstractCartRoute.php +++ b/src/StoreApi/Routes/V1/AbstractCartRoute.php @@ -73,7 +73,7 @@ public function __construct( SchemaController $schema_controller, AbstractSchema $this->cart_schema = $this->schema_controller->get( CartSchema::IDENTIFIER ); $this->cart_item_schema = $this->schema_controller->get( CartItemSchema::IDENTIFIER ); $this->cart_controller = new CartController(); - $this->order_controller = new OrderController(); + $this->order_controller = new OrderController( $this->additional_fields_controller ); } /** diff --git a/src/StoreApi/Routes/V1/AbstractRoute.php b/src/StoreApi/Routes/V1/AbstractRoute.php index 8563cce0c65..d397702e70f 100644 --- a/src/StoreApi/Routes/V1/AbstractRoute.php +++ b/src/StoreApi/Routes/V1/AbstractRoute.php @@ -6,6 +6,8 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException; use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema; +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; +use Automattic\WooCommerce\Blocks\Package; use WP_Error; /** @@ -33,6 +35,13 @@ abstract class AbstractRoute implements RouteInterface { */ protected $schema_controller; + /** + * Checkout fields controller. + * + * @var CheckoutFields + */ + protected CheckoutFields $additional_fields_controller; + /** * The routes schema. * @@ -54,8 +63,9 @@ abstract class AbstractRoute implements RouteInterface { * @param AbstractSchema $schema Schema class for this route. */ public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) { - $this->schema_controller = $schema_controller; - $this->schema = $schema; + $this->schema_controller = $schema_controller; + $this->schema = $schema; + $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); } /** diff --git a/src/StoreApi/Routes/V1/Checkout.php b/src/StoreApi/Routes/V1/Checkout.php index 2f93309be30..fde4c2681a6 100644 --- a/src/StoreApi/Routes/V1/Checkout.php +++ b/src/StoreApi/Routes/V1/Checkout.php @@ -39,12 +39,6 @@ class Checkout extends AbstractCartRoute { */ private $order = null; - /** - * Checkout fields controller. - * - * @var CheckoutFields - */ - private CheckoutFields $additional_fields_controller; /** * Get the path of this REST route. * diff --git a/src/StoreApi/Routes/V1/Order.php b/src/StoreApi/Routes/V1/Order.php index 3fc42d32395..3c8e8592d23 100644 --- a/src/StoreApi/Routes/V1/Order.php +++ b/src/StoreApi/Routes/V1/Order.php @@ -43,7 +43,7 @@ class Order extends AbstractRoute { public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) { parent::__construct( $schema_controller, $schema ); - $this->order_controller = new OrderController(); + $this->order_controller = new OrderController( $this->additional_fields_controller ); } /** diff --git a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index 47cc8fa991b..fabe4fa8f76 100644 --- a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -1,7 +1,6 @@ additional_fields_controller->get_address_fields_keys(); - $additional_fields = array_filter( - $this->additional_fields_controller->get_fields(), - function( $field ) use ( $additional_fields_keys ) { - return in_array( $field['key'], $additional_fields_keys, true ); - } + + $fields = $this->additional_fields_controller->get_fields(); + + $address_fields = array_filter( + $fields, + function( $key ) use ( $additional_fields_keys ) { + return in_array( $key, $additional_fields_keys, true ); + }, + ARRAY_FILTER_USE_KEY ); - $schema = []; - foreach ( $additional_fields as $key => $field ) { + + $schema = []; + foreach ( $address_fields as $key => $field ) { $schema[ $key ] = [ 'description' => $field['label'], 'type' => 'string', diff --git a/src/StoreApi/Schemas/V1/AbstractSchema.php b/src/StoreApi/Schemas/V1/AbstractSchema.php index ea72914f5cc..f7fbea74b44 100644 --- a/src/StoreApi/Schemas/V1/AbstractSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractSchema.php @@ -3,6 +3,8 @@ use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; +use Automattic\WooCommerce\Blocks\Package; /** * AbstractSchema class. @@ -31,6 +33,12 @@ abstract class AbstractSchema { */ protected $controller; + /** + * Checkout fields controller. + * + * @var CheckoutFields + */ + protected CheckoutFields $additional_fields_controller; /** * Extending key that gets added to endpoint. * @@ -45,8 +53,9 @@ abstract class AbstractSchema { * @param SchemaController $controller Schema Controller instance. */ public function __construct( ExtendSchema $extend, SchemaController $controller ) { - $this->extend = $extend; - $this->controller = $controller; + $this->extend = $extend; + $this->controller = $controller; + $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); } /** diff --git a/src/StoreApi/Schemas/V1/CheckoutSchema.php b/src/StoreApi/Schemas/V1/CheckoutSchema.php index ed805a8c537..8563d721f3c 100644 --- a/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -4,7 +4,6 @@ use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; -use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; /** * CheckoutSchema class. @@ -45,13 +44,6 @@ class CheckoutSchema extends AbstractSchema { */ protected $image_attachment_schema; - /** - * Additional fields controller instance. - * - * @var CheckoutFields - */ - private CheckoutFields $additional_fields_controller; - /** * Constructor. * @@ -267,14 +259,19 @@ protected function get_additional_fields_response( \WC_Order $order ) { * @return array */ protected function get_additional_fields_schema() { - $additional_fields_keys = array_merge( $this->additional_fields_controller->get_contact_fields_keys(), $this->additional_fields_controller->get_additional_fields_keys() ); - $additional_fields = array_filter( - $this->additional_fields_controller->get_fields(), - function( $field ) use ( $additional_fields_keys ) { - return in_array( $field['key'], $additional_fields_keys, true ); - } + $additional_fields_keys = $this->additional_fields_controller->get_additional_fields_keys(); + + $fields = $this->additional_fields_controller->get_fields(); + + $additional_fields = array_filter( + $fields, + function( $key ) use ( $additional_fields_keys ) { + return in_array( $key, $additional_fields_keys, true ); + }, + ARRAY_FILTER_USE_KEY ); - $schema = []; + + $schema = []; foreach ( $additional_fields as $key => $field ) { $schema[ $key ] = [ 'description' => $field['label'], diff --git a/src/StoreApi/Schemas/V1/OrderSchema.php b/src/StoreApi/Schemas/V1/OrderSchema.php index 32f090b01d7..62bd123709f 100644 --- a/src/StoreApi/Schemas/V1/OrderSchema.php +++ b/src/StoreApi/Schemas/V1/OrderSchema.php @@ -101,7 +101,7 @@ public function __construct( ExtendSchema $extend, SchemaController $controller $this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER ); $this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER ); $this->error_schema = $this->controller->get( ErrorSchema::IDENTIFIER ); - $this->order_controller = new OrderController(); + $this->order_controller = new OrderController( $this->additional_fields_controller ); } /** diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php index 0c2bf825d84..6bce94bf87b 100644 --- a/src/StoreApi/Utilities/CheckoutTrait.php +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -1,7 +1,6 @@ additional_fields_controller = $additional_fields_controller; + } + /** * Create order and set props based on global settings. * @@ -380,7 +389,15 @@ protected function validate_address_fields( \WC_Order $order, $address_type, \WP * We are not using wc()->counties->get_default_address_fields() here because that is filtered. Instead, this array * is based on assets/js/base/components/cart-checkout/address-form/default-address-fields.js */ - $address_fields = $this->additional_fields_controller->get_fields_for_location( 'address' ); + $fields = $this->additional_fields_controller->get_fields(); + $address_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); + $address_fields = array_filter( + $fields, + function( $key ) use ( $address_fields_keys ) { + return in_array( $key, $address_fields_keys, true ); + }, + ARRAY_FILTER_USE_KEY + ); if ( $current_locale ) { foreach ( $current_locale as $key => $field ) { From 9e705170d3d1cda11b531e2d1db3f0579ad4b066 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Wed, 6 Dec 2023 15:28:23 +0000 Subject: [PATCH 10/46] Require functions from Domain/Services --- src/Domain/Services/CheckoutFields.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 11e1de756c2..8969612d57e 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -66,6 +66,8 @@ class CheckoutFields { * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry. */ public function __construct( AssetDataRegistry $asset_data_registry ) { + require_once __DIR__ . '/functions.php'; + $this->asset_data_registry = $asset_data_registry; $this->core_fields = array( 'email' => array( From 17a604afe33c48bf0cae39c3eee8f53233cc19a5 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Wed, 6 Dec 2023 17:01:28 +0100 Subject: [PATCH 11/46] fix how default address fields was used --- .../address-form/prepare-address-fields.ts | 4 ++-- .../cart-checkout/address-form/test/index.js | 6 ++--- .../context/hooks/use-checkout-address.ts | 6 ++--- assets/js/base/utils/address.ts | 24 ++++++++----------- ...lt-address-fields.ts => default-fields.ts} | 9 ++++--- assets/js/settings/shared/index.ts | 2 +- .../checkout/checkout-flow-and-events.md | 4 ++-- src/Domain/Services/CheckoutFields.php | 4 ++-- src/StoreApi/Utilities/OrderController.php | 2 +- 9 files changed, 28 insertions(+), 33 deletions(-) rename assets/js/settings/shared/{default-address-fields.ts => default-fields.ts} (90%) diff --git a/assets/js/base/components/cart-checkout/address-form/prepare-address-fields.ts b/assets/js/base/components/cart-checkout/address-form/prepare-address-fields.ts index c73d9437f42..a9e89f88d51 100644 --- a/assets/js/base/components/cart-checkout/address-form/prepare-address-fields.ts +++ b/assets/js/base/components/cart-checkout/address-form/prepare-address-fields.ts @@ -7,7 +7,7 @@ import { AddressField, AddressFields, CountryAddressFields, - defaultAddressFields, + defaultFields, KeyedAddressField, LocaleSpecificAddressField, } from '@woocommerce/settings'; @@ -114,7 +114,7 @@ const prepareAddressFields = ( return fields .map( ( field ) => { - const defaultConfig = defaultAddressFields[ field ] || {}; + const defaultConfig = defaultFields[ field ] || {}; const localeConfig = localeConfigs[ field ] || {}; const fieldConfig = fieldConfigs[ field ] || {}; diff --git a/assets/js/base/components/cart-checkout/address-form/test/index.js b/assets/js/base/components/cart-checkout/address-form/test/index.js index 3b04d30e847..c02f519dbd3 100644 --- a/assets/js/base/components/cart-checkout/address-form/test/index.js +++ b/assets/js/base/components/cart-checkout/address-form/test/index.js @@ -5,6 +5,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { CheckoutProvider } from '@woocommerce/base-context'; import { useCheckoutAddress } from '@woocommerce/base-context/hooks'; +import { ADDITIONAL_FIELDS_KEYS } from '@woocommerce/block-settings'; /** * Internal dependencies @@ -81,15 +82,14 @@ const inputAddress = async ( { describe( 'AddressForm Component', () => { const WrappedAddressForm = ( { type } ) => { - const { defaultAddressFields, setShippingAddress, shippingAddress } = - useCheckoutAddress(); + const { setShippingAddress, shippingAddress } = useCheckoutAddress(); return ( ); }; diff --git a/assets/js/base/context/hooks/use-checkout-address.ts b/assets/js/base/context/hooks/use-checkout-address.ts index 15d52462c21..308318d73d3 100644 --- a/assets/js/base/context/hooks/use-checkout-address.ts +++ b/assets/js/base/context/hooks/use-checkout-address.ts @@ -2,7 +2,7 @@ * External dependencies */ import { - defaultAddressFields, + defaultFields, AddressFields, ShippingAddress, BillingAddress, @@ -26,7 +26,7 @@ interface CheckoutAddress { setEmail: ( value: string ) => void; useShippingAsBilling: boolean; setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void; - defaultAddressFields: AddressFields; + defaultFields: AddressFields; showShippingFields: boolean; showBillingFields: boolean; forcedBillingAddress: boolean; @@ -74,7 +74,7 @@ export const useCheckoutAddress = (): CheckoutAddress => { setShippingAddress, setBillingAddress, setEmail, - defaultAddressFields, + defaultFields, useShippingAsBilling, setUseShippingAsBilling: __internalSetUseShippingAsBilling, needsShipping, diff --git a/assets/js/base/utils/address.ts b/assets/js/base/utils/address.ts index 9a41a7e93eb..92c400cb6ac 100644 --- a/assets/js/base/utils/address.ts +++ b/assets/js/base/utils/address.ts @@ -7,16 +7,12 @@ import type { CartResponseBillingAddress, CartResponseShippingAddress, } from '@woocommerce/types'; -import { - AddressFields, - defaultAddressFields, - ShippingAddress, - BillingAddress, -} from '@woocommerce/settings'; +import { ShippingAddress, BillingAddress } from '@woocommerce/settings'; import { decodeEntities } from '@wordpress/html-entities'; import { SHIPPING_COUNTRIES, SHIPPING_STATES, + ADDRESS_FIELDS_KEYS, } from '@woocommerce/block-settings'; /** @@ -26,10 +22,9 @@ export const isSameAddress = < T extends ShippingAddress | BillingAddress >( address1: T, address2: T ): boolean => { - return Object.keys( defaultAddressFields ).every( - ( field: string ) => - address1[ field as keyof T ] === address2[ field as keyof T ] - ); + return Object.keys( ADDRESS_FIELDS_KEYS ).every( ( field: string ) => { + return address1[ field as keyof T ] === address2[ field as keyof T ]; + } ); }; /** @@ -94,10 +89,11 @@ export const emptyHiddenAddressFields = < >( address: T ): T => { - const fields = Object.keys( - defaultAddressFields - ) as ( keyof AddressFields )[]; - const addressFields = prepareAddressFields( fields, {}, address.country ); + const addressFields = prepareAddressFields( + ADDRESS_FIELDS_KEYS, + {}, + address.country + ); const newAddress = Object.assign( {}, address ) as T; addressFields.forEach( ( { key = '', hidden = false } ) => { diff --git a/assets/js/settings/shared/default-address-fields.ts b/assets/js/settings/shared/default-fields.ts similarity index 90% rename from assets/js/settings/shared/default-address-fields.ts rename to assets/js/settings/shared/default-fields.ts index c4f31514cf8..2a547f3760a 100644 --- a/assets/js/settings/shared/default-address-fields.ts +++ b/assets/js/settings/shared/default-fields.ts @@ -68,10 +68,9 @@ export interface BillingAddress extends ShippingAddress { export type CountryAddressFields = Record< string, AddressFields >; /** - * Default address field properties. + * Default field properties. */ -export const defaultAddressFields: AddressFields = getSetting< AddressFields >( - 'defaultAddressFields' -); +export const defaultFields: AddressFields = + getSetting< AddressFields >( 'defaultFields' ); -export default defaultAddressFields; +export default defaultFields; diff --git a/assets/js/settings/shared/index.ts b/assets/js/settings/shared/index.ts index 174a13c8cb1..dc4f1ac52bd 100644 --- a/assets/js/settings/shared/index.ts +++ b/assets/js/settings/shared/index.ts @@ -4,6 +4,6 @@ import '../../filters/exclude-draft-status-from-analytics'; export * from './default-constants'; -export * from './default-address-fields'; +export * from './default-fields'; export * from './utils'; export { allSettings } from './settings-init'; diff --git a/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md b/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md index 15b5c73eff7..757a64425d2 100644 --- a/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md +++ b/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md @@ -276,8 +276,8 @@ const successResponse = { type: 'success' }; When a success response is returned, the payment method context status will be changed to `SUCCESS`. In addition, including any of the additional properties will result in extra actions: - `paymentMethodData`: The contents of this object will be included as the value for `payment_data` when checkout sends a request to the checkout endpoint for processing the order. This is useful if a payment method does additional server side processing. -- `billingAddress`: This allows payment methods to update any billing data information in the checkout (typically used by Express payment methods) so it's included in the checkout processing request to the server. This data should be in the [shape outlined here](../../../../assets/js/settings/shared/default-address-fields.ts). -- `shippingAddress`: This allows payment methods to update any shipping data information for the order (typically used by Express payment methods) so it's included in the checkout processing request to the server. This data should be in the [shape outlined here](../../../../assets/js/settings/shared/default-address-fields.ts). +- `billingAddress`: This allows payment methods to update any billing data information in the checkout (typically used by Express payment methods) so it's included in the checkout processing request to the server. This data should be in the [shape outlined here](../../../../assets/js/settings/shared/default-fields.ts). +- `shippingAddress`: This allows payment methods to update any shipping data information for the order (typically used by Express payment methods) so it's included in the checkout processing request to the server. This data should be in the [shape outlined here](../../../../assets/js/settings/shared/default-fields.ts). If `billingAddress` or `shippingAddress` properties aren't in the response object, then the state for the data is left alone. diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 8969612d57e..a50ce222fb4 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -211,7 +211,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'VAT (optional)', 'woo-gutenberg-products-block' ), - 'required' => false, + 'required' => true, 'hidden' => false, 'autocomplete' => 'vat', 'autocapitalize' => 'characters', @@ -251,7 +251,7 @@ public function init() { * Add fields data to the asset data registry. */ public function add_fields_data() { - $this->asset_data_registry->add( 'defaultAddressFields', $this->get_fields(), true ); + $this->asset_data_registry->add( 'defaultFields', $this->get_fields(), true ); $this->asset_data_registry->add( 'addressFieldsLocations', $this->fields_locations, true ); } diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index 287da532f9a..4ea3cd4d9ea 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -387,7 +387,7 @@ protected function validate_address_fields( \WC_Order $order, $address_type, \WP /** * We are not using wc()->counties->get_default_address_fields() here because that is filtered. Instead, this array - * is based on assets/js/base/components/cart-checkout/address-form/default-address-fields.js + * is based on assets/js/base/components/cart-checkout/address-form/default-fields.js */ $fields = $this->additional_fields_controller->get_fields(); $address_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); From 5140eaa9eb5b06eb912ba1497057ea7f9648ca52 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Wed, 6 Dec 2023 17:55:26 +0000 Subject: [PATCH 12/46] Update order address with additional fields --- src/Domain/Services/CheckoutFields.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index a50ce222fb4..f949232afb9 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -237,6 +237,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { // @todo handle rendering additional fields. 'additional' => array( 'plugin_delivery_hour' ), // everything here will only be saved to order only. ); + add_filter( 'woocommerce_get_order_address', array( $this, 'add_additional_fields_to_address' ), 10, 3 ); } /** @@ -247,6 +248,27 @@ public function init() { add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) ); } + /** + * Update the address object with fields from the order. + * + * @param mixed $address The address to modify. + * @param string $address_type The address type. + * @param \WC_Order $order The order to get the additional field values from. + * @return mixed + */ + public function add_additional_fields_to_address( $address, $address_type, $order ) { + $additional_fields = $this->get_all_fields_from_order( $order ); + + // Get additional fields on the order and insert them into the address. By default get_address does not include additional fields. + foreach ( $additional_fields as $field_id => $field_value ) { + $prefix = '/' . $address_type . '/'; + if ( strpos( $field_id, $prefix ) === 0 ) { + $address[ str_replace( $prefix, '', $field_id ) ] = $field_value; + } + } + return $address; + } + /** * Add fields data to the asset data registry. */ From 25f4394c07c2954b02b9b10a4783cb3d2b81fe2d Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Wed, 6 Dec 2023 17:57:34 +0000 Subject: [PATCH 13/46] Update default locale with fields without country limitation --- src/Domain/Services/CheckoutFields.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index f949232afb9..a6c4204192e 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -238,6 +238,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'additional' => array( 'plugin_delivery_hour' ), // everything here will only be saved to order only. ); add_filter( 'woocommerce_get_order_address', array( $this, 'add_additional_fields_to_address' ), 10, 3 ); + add_filter( 'woocommerce_get_country_locale_default', array( $this, 'update_default_locale_with_fields' ) ); } /** @@ -269,6 +270,21 @@ public function add_additional_fields_to_address( $address, $address_type, $orde return $address; } + /** + * Update the default locale with additional fields without country limitations. + * + * @param array $locale The locale to update. + * @return mixed + */ + public function update_default_locale_with_fields( $locale ) { + foreach ( $this->additional_fields as $field_id => $additional_field ) { + if ( empty( $locale[ $field_id ] ) ) { + $locale[ $field_id ] = $additional_field; + } + } + return $locale; + } + /** * Add fields data to the asset data registry. */ From d1fcd853f21f55eac80efd14875b07b093b0ead6 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Wed, 6 Dec 2023 17:59:00 +0000 Subject: [PATCH 14/46] Update country locale with fields with country limitations --- src/Domain/Services/CheckoutFields.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index a6c4204192e..54e4b6dcf20 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -238,6 +238,7 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'additional' => array( 'plugin_delivery_hour' ), // everything here will only be saved to order only. ); add_filter( 'woocommerce_get_order_address', array( $this, 'add_additional_fields_to_address' ), 10, 3 ); + add_filter( 'woocommerce_get_country_locale', array( $this, 'update_country_locale_with_fields' ) ); add_filter( 'woocommerce_get_country_locale_default', array( $this, 'update_default_locale_with_fields' ) ); } @@ -285,6 +286,27 @@ public function update_default_locale_with_fields( $locale ) { return $locale; } + /** + * Update country-specific locales with additional fields that specify a country limitation. + * + * @param array $locales The locales to update. + * @return mixed + */ + public function update_country_locale_with_fields( $locales ) { + foreach ( $this->additional_fields as $field_id => $additional_field ) { + // If field has country limitation only push into that country. + if ( ! empty( $additional_field['country_limitation'] ) && is_array( $additional_field['country_limitation'] ) && count( array_intersect( array_keys( $locales ), $additional_field['country_limitation'] ) ) > 0 ) { + foreach ( $additional_field['country_limitation'] as $country ) { + if ( ! empty( $locales[ $country ][ $field_id ] ) ) { + continue; + } + $locales[ $country ][ $field_id ] = $additional_field; + } + } + } + return $locales; + } + /** * Add fields data to the asset data registry. */ From 4ab27fde95c1361e1cdb2e3154904f3898c4c720 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Wed, 6 Dec 2023 17:59:26 +0000 Subject: [PATCH 15/46] Allow country_limitation to be specified when registering fields --- src/Domain/Services/CheckoutFields.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 54e4b6dcf20..3347cb469c0 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -350,12 +350,13 @@ public function register_checkout_field( $options ) { // Insert new field into the correct location array. $this->additional_fields[ $id ] = array( - 'label' => $options['label'], - 'optionalLabel' => ! empty( $options['optionalLabel'] ) ? $options['optionalLabel'] : '', - 'required' => ! empty( $options['required'] ) ? $options['required'] : false, - 'hidden' => ! empty( $options['hidden'] ) ? $options['hidden'] : false, - 'autocomplete' => ! empty( $options['autocomplete'] ) ? $options['autocomplete'] : '', - 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', + 'label' => $options['label'], + 'optionalLabel' => ! empty( $options['optionalLabel'] ) ? $options['optionalLabel'] : '', + 'required' => ! empty( $options['required'] ) ? $options['required'] : false, + 'hidden' => ! empty( $options['hidden'] ) ? $options['hidden'] : false, + 'autocomplete' => ! empty( $options['autocomplete'] ) ? $options['autocomplete'] : '', + 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', + 'country_limitation' => ! empty( $options['country_limitation'] ) ? $options['country_limitation'] : '', ); array_push( $this->fields_locations[ $location ], $id ); From 8f0c8b4db60dba96a66af59922b5cb78731baae0 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Wed, 6 Dec 2023 19:37:58 +0100 Subject: [PATCH 16/46] persist user meta --- src/Domain/Services/CheckoutFields.php | 10 +++++++++- src/StoreApi/Routes/V1/CartUpdateCustomer.php | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 3347cb469c0..e07feaad8e1 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -548,6 +548,7 @@ public function get_all_fields_from_order( $order ) { */ private function set_array_meta( $key, $value, $object ) { $meta_key = ''; + if ( 0 === strpos( $key, '/billing/' ) ) { $meta_key = self::BILLING_FIELDS_KEY; $key = str_replace( '/billing/', '', $key ); @@ -562,8 +563,15 @@ private function set_array_meta( $key, $value, $object ) { if ( ! is_array( $meta_data ) ) { $meta_data = array(); } + $meta_data[ $key ] = $value; - $object->update_meta_data( $meta_key, $meta_data ); + // @TODO: figure out why calling `set_meta_data` on WC_Customer isn't persisting the data. + if ( $object instanceof \WC_Customer ) { + \update_user_meta( $object->get_id(), $meta_key, $meta_data ); + } elseif ( $object instanceof \WC_Order ) { + $object->set_meta_data( $meta_key, $meta_data ); + } + } /** diff --git a/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/src/StoreApi/Routes/V1/CartUpdateCustomer.php index 2ec9f0bc4a0..ad48dce4f05 100644 --- a/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -239,7 +239,7 @@ protected function get_customer_billing_address( \WC_Customer $customer ) { $additional_fields = array_reduce( array_keys( $additional_fields ), - function( $carry, $key, $value ) use ( $additional_fields ) { + function( $carry, $key ) use ( $additional_fields ) { if ( 0 === strpos( $key, '/billing/' ) ) { $value = $additional_fields[ $key ]; $key = str_replace( '/billing/', '', $key ); @@ -288,7 +288,7 @@ protected function get_customer_shipping_address( \WC_Customer $customer ) { $additional_fields = array_reduce( array_keys( $additional_fields ), - function( $carry, $key, $value ) use ( $additional_fields ) { + function( $carry, $key ) use ( $additional_fields ) { if ( 0 === strpos( $key, '/shipping/' ) ) { $value = $additional_fields[ $key ]; $key = str_replace( '/shipping/', '', $key ); From 2f0c1b3be0cc963c2c6d3c25607f33e2dea147be Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 7 Dec 2023 13:17:31 +0000 Subject: [PATCH 17/46] Remove country limitation specific code --- src/Domain/Services/CheckoutFields.php | 35 +++++--------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index e07feaad8e1..8a428171768 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -238,7 +238,6 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { 'additional' => array( 'plugin_delivery_hour' ), // everything here will only be saved to order only. ); add_filter( 'woocommerce_get_order_address', array( $this, 'add_additional_fields_to_address' ), 10, 3 ); - add_filter( 'woocommerce_get_country_locale', array( $this, 'update_country_locale_with_fields' ) ); add_filter( 'woocommerce_get_country_locale_default', array( $this, 'update_default_locale_with_fields' ) ); } @@ -286,27 +285,6 @@ public function update_default_locale_with_fields( $locale ) { return $locale; } - /** - * Update country-specific locales with additional fields that specify a country limitation. - * - * @param array $locales The locales to update. - * @return mixed - */ - public function update_country_locale_with_fields( $locales ) { - foreach ( $this->additional_fields as $field_id => $additional_field ) { - // If field has country limitation only push into that country. - if ( ! empty( $additional_field['country_limitation'] ) && is_array( $additional_field['country_limitation'] ) && count( array_intersect( array_keys( $locales ), $additional_field['country_limitation'] ) ) > 0 ) { - foreach ( $additional_field['country_limitation'] as $country ) { - if ( ! empty( $locales[ $country ][ $field_id ] ) ) { - continue; - } - $locales[ $country ][ $field_id ] = $additional_field; - } - } - } - return $locales; - } - /** * Add fields data to the asset data registry. */ @@ -350,13 +328,12 @@ public function register_checkout_field( $options ) { // Insert new field into the correct location array. $this->additional_fields[ $id ] = array( - 'label' => $options['label'], - 'optionalLabel' => ! empty( $options['optionalLabel'] ) ? $options['optionalLabel'] : '', - 'required' => ! empty( $options['required'] ) ? $options['required'] : false, - 'hidden' => ! empty( $options['hidden'] ) ? $options['hidden'] : false, - 'autocomplete' => ! empty( $options['autocomplete'] ) ? $options['autocomplete'] : '', - 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', - 'country_limitation' => ! empty( $options['country_limitation'] ) ? $options['country_limitation'] : '', + 'label' => $options['label'], + 'optionalLabel' => ! empty( $options['optionalLabel'] ) ? $options['optionalLabel'] : '', + 'required' => ! empty( $options['required'] ) ? $options['required'] : false, + 'hidden' => ! empty( $options['hidden'] ) ? $options['hidden'] : false, + 'autocomplete' => ! empty( $options['autocomplete'] ) ? $options['autocomplete'] : '', + 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', ); array_push( $this->fields_locations[ $location ], $id ); From 42fbad60f8c52c4d3acdc2d081ab9f25fa1b9d5a Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Thu, 7 Dec 2023 15:55:11 +0100 Subject: [PATCH 18/46] add support for saving data in session --- src/Domain/Services/CheckoutFields.php | 73 +++++++++++++++++++++----- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 8a428171768..86cf7bde12b 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -500,7 +500,25 @@ public function get_field_from_order( $field, $order, $group = '' ) { * @return array An array of fields. */ public function get_all_fields_from_customer( $customer ) { - return $this->get_all_fields_from_object( $customer ); + $customer_id = $customer->get_id(); + $meta_data = [ + 'billing' => [], + 'shipping' => [], + 'additional' => [], + ]; + if ( ! $customer_id ) { + if ( isset( wc()->session ) ) { + $meta_data['billing'] = wc()->session->get( self::BILLING_FIELDS_KEY, [] ); + $meta_data['shipping'] = wc()->session->get( self::SHIPPING_FIELDS_KEY, [] ); + $meta_data['additional'] = wc()->session->get( self::ADDITIONAL_FIELDS_KEY, [] ); + } + } else { + $meta_data['billing'] = get_user_meta( $customer_id, self::BILLING_FIELDS_KEY, true ); + $meta_data['shipping'] = get_user_meta( $customer_id, self::SHIPPING_FIELDS_KEY, true ); + $meta_data['additional'] = get_user_meta( $customer_id, self::ADDITIONAL_FIELDS_KEY, true ); + } + + return $this->format_meta_data( $meta_data ); } /** @@ -511,7 +529,17 @@ public function get_all_fields_from_customer( $customer ) { * @return array An array of fields. */ public function get_all_fields_from_order( $order ) { - return $this->get_all_fields_from_object( $order ); + $meta_data = [ + 'billing' => [], + 'shipping' => [], + 'additional' => [], + ]; + if ( $order instanceof \WC_Order ) { + $meta_data['billing'] = $order->get_meta( self::BILLING_FIELDS_KEY, true ); + $meta_data['shipping'] = $order->get_meta( self::SHIPPING_FIELDS_KEY, true ); + $meta_data['additional'] = $order->get_meta( self::ADDITIONAL_FIELDS_KEY, true ); + } + return $this->format_meta_data( $meta_data ); } /** @@ -536,7 +564,16 @@ private function set_array_meta( $key, $value, $object ) { $meta_key = self::ADDITIONAL_FIELDS_KEY; } - $meta_data = $object->get_meta( $meta_key, true ); + if ( $object instanceof \WC_Customer ) { + if ( ! $object->get_id() ) { + $meta_data = wc()->session->get( $meta_key, array() ); + } else { + $meta_data = get_user_meta( $object->get_id(), $meta_key, true ); + } + } elseif ( $object instanceof \WC_Order ) { + $meta_data = $object->get_meta( $meta_key, true ); + } + if ( ! is_array( $meta_data ) ) { $meta_data = array(); } @@ -544,9 +581,13 @@ private function set_array_meta( $key, $value, $object ) { $meta_data[ $key ] = $value; // @TODO: figure out why calling `set_meta_data` on WC_Customer isn't persisting the data. if ( $object instanceof \WC_Customer ) { - \update_user_meta( $object->get_id(), $meta_key, $meta_data ); + if ( ! $object->get_id() ) { + wc()->session->set( $meta_key, $meta_data ); + } else { + update_user_meta( $object->get_id(), $meta_key, $meta_data ); + } } elseif ( $object instanceof \WC_Order ) { - $object->set_meta_data( $meta_key, $meta_data ); + $object->update_meta_data( $meta_key, $meta_data ); } } @@ -572,7 +613,15 @@ private function get_field_from_object( $key, $object, $group = '' ) { $meta_key = self::ADDITIONAL_FIELDS_KEY; } - $meta_data = $object->get_meta( $meta_key, true ); + if ( $object instanceof \WC_Customer ) { + if ( ! $object->get_id() ) { + $meta_data = wc()->session->get( $meta_key, array() ); + } else { + $meta_data = get_user_meta( $object->get_id(), $meta_key, true ); + } + } elseif ( $object instanceof \WC_Order ) { + $meta_data = $object->get_meta( $meta_key, true ); + } if ( ! is_array( $meta_data ) ) { return ''; @@ -586,16 +635,16 @@ private function get_field_from_object( $key, $object, $group = '' ) { } /** - * Returns an array of all fields values for a given object. It would add the billing or shipping prefix to the keys. + * Returns an array of all fields values for a given meta object. It would add the billing or shipping prefix to the keys. * - * @param \WC_Order|\WC_Customer $object The object to get the fields for. + * @param array $meta The meta data to format. * * @return array An array of fields. */ - private function get_all_fields_from_object( $object ) { - $billing_fields = $object->get_meta( self::BILLING_FIELDS_KEY, true ); - $shipping_fields = $object->get_meta( self::SHIPPING_FIELDS_KEY, true ); - $additional_fields = $object->get_meta( self::ADDITIONAL_FIELDS_KEY, true ); + private function format_meta_data( $meta ) { + $billing_fields = $meta['billing'] ?? []; + $shipping_fields = $meta['shipping'] ?? []; + $additional_fields = $meta['additional'] ?? []; $fields = array(); From a368f4213390155d309c8bcbc2ff6d5c90b21449 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Thu, 7 Dec 2023 15:56:49 +0100 Subject: [PATCH 19/46] add todo to copy fields upon acount creation --- src/StoreApi/Routes/V1/Checkout.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StoreApi/Routes/V1/Checkout.php b/src/StoreApi/Routes/V1/Checkout.php index fde4c2681a6..c124a3fd4ff 100644 --- a/src/StoreApi/Routes/V1/Checkout.php +++ b/src/StoreApi/Routes/V1/Checkout.php @@ -508,6 +508,7 @@ private function process_customer( \WP_REST_Request $request ) { // Associate customer with the order. This is done before login to ensure the order is associated with // the correct customer if login fails. + // @TODO: copy custom shipping/billing fields from the session to the newly created customer. $this->order->set_customer_id( $customer_id ); $this->order->save(); From d323cd1159e6c1c884310d06f04a7fbcbdb1a80a Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 7 Dec 2023 16:47:59 +0000 Subject: [PATCH 20/46] Update comment typo --- src/Domain/Services/CheckoutFields.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 86cf7bde12b..bc5b33d7547 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -362,7 +362,7 @@ public function get_fields_for_location( $location ) { } /** - * Returns an array of fields keys for a the address group. + * Returns an array of fields keys for the address group. * * @return array An array of fields keys. */ @@ -371,7 +371,7 @@ public function get_address_fields_keys() { } /** - * Returns an array of fields keys for a the contact group. + * Returns an array of fields keys for the contact group. * * @return array An array of fields keys. */ @@ -380,7 +380,7 @@ public function get_contact_fields_keys() { } /** - * Returns an array of fields keys for a the additional area group. + * Returns an array of fields keys for the additional area group. * * @return array An array of fields keys. */ From 443a1337270356f42235869c25789a2c5f9ee1fc Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 7 Dec 2023 16:54:32 +0000 Subject: [PATCH 21/46] Move address field fetching to order controller --- src/Domain/Services/CheckoutFields.php | 22 ---------------------- src/StoreApi/Utilities/OrderController.php | 9 +++++++++ 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index bc5b33d7547..47a3c5d1e8a 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -237,7 +237,6 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { // @todo handle rendering additional fields. 'additional' => array( 'plugin_delivery_hour' ), // everything here will only be saved to order only. ); - add_filter( 'woocommerce_get_order_address', array( $this, 'add_additional_fields_to_address' ), 10, 3 ); add_filter( 'woocommerce_get_country_locale_default', array( $this, 'update_default_locale_with_fields' ) ); } @@ -249,27 +248,6 @@ public function init() { add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) ); } - /** - * Update the address object with fields from the order. - * - * @param mixed $address The address to modify. - * @param string $address_type The address type. - * @param \WC_Order $order The order to get the additional field values from. - * @return mixed - */ - public function add_additional_fields_to_address( $address, $address_type, $order ) { - $additional_fields = $this->get_all_fields_from_order( $order ); - - // Get additional fields on the order and insert them into the address. By default get_address does not include additional fields. - foreach ( $additional_fields as $field_id => $field_value ) { - $prefix = '/' . $address_type . '/'; - if ( strpos( $field_id, $prefix ) === 0 ) { - $address[ str_replace( $prefix, '', $field_id ) ] = $field_value; - } - } - return $address; - } - /** * Update the default locale with additional fields without country limitations. * diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index 4ea3cd4d9ea..a97b8f06f44 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -385,6 +385,15 @@ protected function validate_address_fields( \WC_Order $order, $address_type, \WP $address = $order->get_address( $address_type ); $current_locale = isset( $all_locales[ $address['country'] ] ) ? $all_locales[ $address['country'] ] : []; + $additional_fields = $this->additional_fields_controller->get_all_fields_from_order( $order ); + + foreach ( $additional_fields as $field_id => $field_value ) { + $prefix = '/' . $address_type . '/'; + if ( strpos( $field_id, $prefix ) === 0 ) { + $address[ str_replace( $prefix, '', $field_id ) ] = $field_value; + } + } + /** * We are not using wc()->counties->get_default_address_fields() here because that is filtered. Instead, this array * is based on assets/js/base/components/cart-checkout/address-form/default-fields.js From fa8e4c857f07954d8cc625b78faba90cb41c91cf Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 7 Dec 2023 17:19:13 +0000 Subject: [PATCH 22/46] Remove hardcoded test fields --- src/Domain/Services/CheckoutFields.php | 27 +------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 47a3c5d1e8a..951598113ff 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -204,34 +204,9 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { ), ); - $this->additional_fields = array( - 'plugin_vat' => array( - 'label' => __( 'VAT', 'woo-gutenberg-products-block' ), - 'optionalLabel' => __( - 'VAT (optional)', - 'woo-gutenberg-products-block' - ), - 'required' => true, - 'hidden' => false, - 'autocomplete' => 'vat', - 'autocapitalize' => 'characters', - ), - 'plugin_delivery_hour' => array( - 'label' => __( 'VAT', 'woo-gutenberg-products-block' ), - 'optionalLabel' => __( - 'VAT (optional)', - 'woo-gutenberg-products-block' - ), - 'required' => false, - 'hidden' => false, - 'autocomplete' => 'vat', - 'autocapitalize' => 'characters', - ), - ); - $this->fields_locations = array( // omit email from shipping and billing fields. - 'address' => array_merge( \array_diff_key( array_keys( $this->core_fields ), array( 'email' ) ), array( 'plugin_vat' ) ), // everything here will be saved to customer and order. + 'address' => array_merge( \array_diff_key( array_keys( $this->core_fields ), array( 'email' ) ) ), // everything here will be saved to customer and order. // @todo handle rendering contact fields. 'contact' => array( 'email' ), // everything here will be saved to order, and optionally to customer. // @todo handle rendering additional fields. From d48c6130f9b47f3550f1a38e3bf5067510295cff Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 7 Dec 2023 17:19:37 +0000 Subject: [PATCH 23/46] Ensure fields are added to correct place when registered --- src/Domain/Services/CheckoutFields.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 951598113ff..1c63d3b94b2 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -230,7 +230,7 @@ public function init() { * @return mixed */ public function update_default_locale_with_fields( $locale ) { - foreach ( $this->additional_fields as $field_id => $additional_field ) { + foreach ( $this->fields_locations['address'] as $field_id => $additional_field ) { if ( empty( $locale[ $field_id ] ) ) { $locale[ $field_id ] = $additional_field; } @@ -289,7 +289,7 @@ public function register_checkout_field( $options ) { 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', ); - array_push( $this->fields_locations[ $location ], $id ); + $this->fields_locations[ $location ][] = $id; } /** From 36f14603bac23a07b81ae4de1383072d636e2af1 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 7 Dec 2023 17:21:52 +0000 Subject: [PATCH 24/46] Ensure fields are updated when changing custom value for first time --- assets/js/data/cart/utils.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/assets/js/data/cart/utils.ts b/assets/js/data/cart/utils.ts index 40d0aef5096..9a7b69376b5 100644 --- a/assets/js/data/cart/utils.ts +++ b/assets/js/data/cart/utils.ts @@ -45,7 +45,8 @@ export const shippingAddressHasValidationErrors = () => { export type BaseAddressKey = | keyof CartBillingAddress - | keyof CartShippingAddress; + | keyof CartShippingAddress + | string; // string here because custom checkout fields can be added with arbitrary keys. /** * Normalizes address values before push. @@ -82,12 +83,23 @@ export const getDirtyKeys = < previousAddress ) as BaseAddressKey[]; - return previousAddressKeys.filter( ( key: BaseAddressKey ) => { - return ( - normalizeAddressProp( key, previousAddress[ key ] ) !== - normalizeAddressProp( key, address[ key ] ) - ); - } ); + const addedKeys = Object.keys( address ).filter( + ( key ) => ! previousAddressKeys.includes( key ) + ); + + const removedKeys = previousAddressKeys.filter( + ( key ) => ! Object.keys( address ).includes( key ) + ); + + return previousAddressKeys + .filter( ( key: BaseAddressKey ) => { + return ( + normalizeAddressProp( key, previousAddress[ key ] ) !== + normalizeAddressProp( key, address[ key ] ) + ); + } ) + .concat( removedKeys ) + .concat( addedKeys ); }; /** From 041d7241d01a87ca11762207603f889cc17738b7 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Thu, 7 Dec 2023 17:30:09 +0000 Subject: [PATCH 25/46] Do not create field registration function unless build is experimental --- src/Domain/Services/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Services/functions.php b/src/Domain/Services/functions.php index 2d3b4c1388b..41532f3cf4d 100644 --- a/src/Domain/Services/functions.php +++ b/src/Domain/Services/functions.php @@ -2,7 +2,7 @@ use Automattic\WooCommerce\Blocks\Package; -if ( ! function_exists( 'woocommerce_blocks_register_checkout_field' ) ) { +if ( ! function_exists( 'woocommerce_blocks_register_checkout_field' ) && Package::feature()->is_experimental_build() ) { /** * Register a checkout field. From 552b0f52cf12b266992862b49a627aa0ecbdca72 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 12:46:52 +0100 Subject: [PATCH 26/46] clean up code --- .../cart-checkout/address-form/test/index.js | 4 ++-- composer.json | 5 ++-- src/Domain/Services/CheckoutFields.php | 23 ++++++++++--------- src/Domain/Services/functions.php | 16 ------------- src/StoreApi/Routes/V1/Checkout.php | 2 +- src/StoreApi/Routes/V1/CheckoutOrder.php | 1 - .../Schemas/V1/AbstractAddressSchema.php | 7 ++---- src/StoreApi/Schemas/V1/CartSchema.php | 1 - src/StoreApi/Schemas/V1/CheckoutSchema.php | 1 + src/StoreApi/StoreApi.php | 1 - src/StoreApi/Utilities/CheckoutTrait.php | 3 --- src/StoreApi/Utilities/OrderController.php | 5 ---- 12 files changed, 21 insertions(+), 48 deletions(-) diff --git a/assets/js/base/components/cart-checkout/address-form/test/index.js b/assets/js/base/components/cart-checkout/address-form/test/index.js index c02f519dbd3..f594641d5c3 100644 --- a/assets/js/base/components/cart-checkout/address-form/test/index.js +++ b/assets/js/base/components/cart-checkout/address-form/test/index.js @@ -5,7 +5,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { CheckoutProvider } from '@woocommerce/base-context'; import { useCheckoutAddress } from '@woocommerce/base-context/hooks'; -import { ADDITIONAL_FIELDS_KEYS } from '@woocommerce/block-settings'; +import { ADDRESS_FIELDS_KEYS } from '@woocommerce/block-settings'; /** * Internal dependencies @@ -89,7 +89,7 @@ describe( 'AddressForm Component', () => { type={ type } onChange={ setShippingAddress } values={ shippingAddress } - fields={ ADDITIONAL_FIELDS_KEYS } + fields={ ADDRESS_FIELDS_KEYS } /> ); }; diff --git a/composer.json b/composer.json index c42d8f8411f..7404cc8bffd 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ }, "files": [ "src/StoreApi/deprecated.php", - "src/StoreApi/functions.php" + "src/StoreApi/functions.php", + "src/Domain/Services/functions.php" ] }, "autoload-dev": { @@ -69,4 +70,4 @@ "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" } } -} +} \ No newline at end of file diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 1c63d3b94b2..066cfbb938e 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -66,8 +66,6 @@ class CheckoutFields { * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry. */ public function __construct( AssetDataRegistry $asset_data_registry ) { - require_once __DIR__ . '/functions.php'; - $this->asset_data_registry = $asset_data_registry; $this->core_fields = array( 'email' => array( @@ -206,20 +204,18 @@ public function __construct( AssetDataRegistry $asset_data_registry ) { $this->fields_locations = array( // omit email from shipping and billing fields. - 'address' => array_merge( \array_diff_key( array_keys( $this->core_fields ), array( 'email' ) ) ), // everything here will be saved to customer and order. - // @todo handle rendering contact fields. - 'contact' => array( 'email' ), // everything here will be saved to order, and optionally to customer. - // @todo handle rendering additional fields. - 'additional' => array( 'plugin_delivery_hour' ), // everything here will only be saved to order only. + 'address' => array_merge( \array_diff_key( array_keys( $this->core_fields ), array( 'email' ) ) ), + 'contact' => array( 'email' ), + 'additional' => array(), ); + add_filter( 'woocommerce_get_country_locale_default', array( $this, 'update_default_locale_with_fields' ) ); } /** - * Initialize hooks. + * Initialize hooks. This is not run Store API requests. */ public function init() { - // @TODO: this should move to a class that only run on UI operations. add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) ); } @@ -258,6 +254,13 @@ public function register_checkout_field( $options ) { return new \WP_Error( 'woocommerce_blocks_checkout_field_id_required', __( 'The field id is required.', 'woo-gutenberg-products-block' ) ); } + list( $namespace, $name ) = explode( '/', $options['id'] ); + + // Having $name empty means they didn't pass a namespace. + if ( empty( $name ) ) { + return new \WP_Error( 'woocommerce_blocks_checkout_field_namespace_required', __( 'An id must consist of namespace/name.', 'woo-gutenberg-products-block' ) ); + } + if ( empty( $options['label'] ) ) { return new \WP_Error( 'woocommerce_blocks_checkout_field_label_required', __( 'The field label is required.', 'woo-gutenberg-products-block' ) ); } @@ -273,7 +276,6 @@ public function register_checkout_field( $options ) { // At this point, the essentials fields and its location should be set. $location = $options['location']; $id = $options['id']; - // Check to see if field is already in the array. if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) { return new \WP_Error( 'woocommerce_blocks_checkout_field_already_registered', __( 'The field is already registered.', 'woo-gutenberg-products-block' ) ); @@ -532,7 +534,6 @@ private function set_array_meta( $key, $value, $object ) { } $meta_data[ $key ] = $value; - // @TODO: figure out why calling `set_meta_data` on WC_Customer isn't persisting the data. if ( $object instanceof \WC_Customer ) { if ( ! $object->get_id() ) { wc()->session->set( $meta_key, $meta_data ); diff --git a/src/Domain/Services/functions.php b/src/Domain/Services/functions.php index 41532f3cf4d..1ae1ae3df5e 100644 --- a/src/Domain/Services/functions.php +++ b/src/Domain/Services/functions.php @@ -18,19 +18,3 @@ function woocommerce_blocks_register_checkout_field( $options ) { } } } - -/** - * Example code to register a checkout field. - */ -woocommerce_blocks_register_checkout_field( - array( - 'id' => 'plugin/dialling-code', - 'label' => __( 'Dialling code', 'woo-gutenberg-products-block' ), - 'optionalLabel' => __( 'Dialling code (optional)', 'woo-gutenberg-products-block' ), - 'required' => false, - 'hidden' => true, - 'autocomplete' => 'dialling-code', - 'autocapitalize' => 'characters', - 'location' => 'address', - ) -); diff --git a/src/StoreApi/Routes/V1/Checkout.php b/src/StoreApi/Routes/V1/Checkout.php index c124a3fd4ff..fd91b18b8df 100644 --- a/src/StoreApi/Routes/V1/Checkout.php +++ b/src/StoreApi/Routes/V1/Checkout.php @@ -413,6 +413,7 @@ private function create_or_update_draft_order( \WP_REST_Request $request ) { */ private function update_customer_from_request( \WP_REST_Request $request ) { $customer = wc()->customer; + // Billing address is a required field. foreach ( $request['billing_address'] as $key => $value ) { if ( is_callable( [ $customer, "set_billing_$key" ] ) ) { @@ -508,7 +509,6 @@ private function process_customer( \WP_REST_Request $request ) { // Associate customer with the order. This is done before login to ensure the order is associated with // the correct customer if login fails. - // @TODO: copy custom shipping/billing fields from the session to the newly created customer. $this->order->set_customer_id( $customer_id ); $this->order->save(); diff --git a/src/StoreApi/Routes/V1/CheckoutOrder.php b/src/StoreApi/Routes/V1/CheckoutOrder.php index c411a19e826..48941bb2a18 100644 --- a/src/StoreApi/Routes/V1/CheckoutOrder.php +++ b/src/StoreApi/Routes/V1/CheckoutOrder.php @@ -7,7 +7,6 @@ use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait; use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait; -// Custom Fields Note: This doesn't support custom fields fully yet. /** * CheckoutOrder class. */ diff --git a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index fabe4fa8f76..2346ba1dbff 100644 --- a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -9,8 +9,6 @@ * Provides a generic address schema for composition in other schemas. */ abstract class AbstractAddressSchema extends AbstractSchema { - - /** * Term properties. * @@ -95,9 +93,8 @@ public function get_properties() { */ public function sanitize_callback( $address, $request, $param ) { $validation_util = new ValidationUtils(); - // Custom Fields Note: this doesn't support Select or Checkbox yet, only Text. - $address = array_merge( array_fill_keys( array_keys( $this->get_properties() ), '' ), (array) $address ); - $address = array_reduce( + $address = array_merge( array_fill_keys( array_keys( $this->get_properties() ), '' ), (array) $address ); + $address = array_reduce( array_keys( $address ), function( $carry, $key ) use ( $address, $validation_util ) { if ( 'country' === $key ) { diff --git a/src/StoreApi/Schemas/V1/CartSchema.php b/src/StoreApi/Schemas/V1/CartSchema.php index 0f1c2d86d46..3489c1cad67 100644 --- a/src/StoreApi/Schemas/V1/CartSchema.php +++ b/src/StoreApi/Schemas/V1/CartSchema.php @@ -366,7 +366,6 @@ public function get_item_response( $cart ) { 'errors' => $cart_errors, 'payment_methods' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ), self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ), - ]; } diff --git a/src/StoreApi/Schemas/V1/CheckoutSchema.php b/src/StoreApi/Schemas/V1/CheckoutSchema.php index 8563d721f3c..0a648c8441b 100644 --- a/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -5,6 +5,7 @@ use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; + /** * CheckoutSchema class. */ diff --git a/src/StoreApi/StoreApi.php b/src/StoreApi/StoreApi.php index ede88a1d126..80f4250287f 100644 --- a/src/StoreApi/StoreApi.php +++ b/src/StoreApi/StoreApi.php @@ -17,7 +17,6 @@ * StoreApi Main Class. */ final class StoreApi { - /** * Init and hook in Store API functionality. * diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php index 6bce94bf87b..15d24142201 100644 --- a/src/StoreApi/Utilities/CheckoutTrait.php +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -11,7 +11,6 @@ * Shared functionality for checkout route. */ trait CheckoutTrait { - /** * Prepare a single item for response. Handles setting the status based on the payment result. * @@ -131,7 +130,6 @@ private function update_order_from_request( \WP_REST_Request $request ) { $this->order->set_customer_note( $request['customer_note'] ?? '' ); $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); $this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) ); - $this->persist_additional_fields_for_order( $request ); wc_do_deprecated_action( @@ -192,7 +190,6 @@ private function get_request_payment_method_title( \WP_REST_Request $request ) { * @throws RouteException On error. */ private function persist_additional_fields_for_order( \WP_REST_Request $request ) { - // @TODO: finish this function to actually throw errors. $errors = new \WP_Error(); $request_fields = $request['additional_fields'] ?? []; foreach ( $request_fields as $key => $value ) { diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index a97b8f06f44..b2f52dfb4b2 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -380,7 +380,6 @@ protected function validate_allowed_country( $country, array $allowed_countries * @param \WP_Error $errors Error object. */ protected function validate_address_fields( \WC_Order $order, $address_type, \WP_Error $errors ) { - // Ideally get_country_locale would already include additional fields. $all_locales = wc()->countries->get_country_locale(); $address = $order->get_address( $address_type ); $current_locale = isset( $all_locales[ $address['country'] ] ) ? $all_locales[ $address['country'] ] : []; @@ -394,10 +393,6 @@ protected function validate_address_fields( \WC_Order $order, $address_type, \WP } } - /** - * We are not using wc()->counties->get_default_address_fields() here because that is filtered. Instead, this array - * is based on assets/js/base/components/cart-checkout/address-form/default-fields.js - */ $fields = $this->additional_fields_controller->get_fields(); $address_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); $address_fields = array_filter( From 60fb0b6f77d47bff4d96831341a60f643a1df778 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 13:00:34 +0100 Subject: [PATCH 27/46] clean up checkoutFields functions --- src/Domain/Services/CheckoutFields.php | 199 ++++++++++----------- src/StoreApi/Schemas/V1/CheckoutSchema.php | 12 +- src/StoreApi/Utilities/CheckoutTrait.php | 2 +- src/StoreApi/Utilities/OrderController.php | 2 +- 4 files changed, 109 insertions(+), 106 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 066cfbb938e..d20ecac3760 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -219,21 +219,6 @@ public function init() { add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) ); } - /** - * Update the default locale with additional fields without country limitations. - * - * @param array $locale The locale to update. - * @return mixed - */ - public function update_default_locale_with_fields( $locale ) { - foreach ( $this->fields_locations['address'] as $field_id => $additional_field ) { - if ( empty( $locale[ $field_id ] ) ) { - $locale[ $field_id ] = $additional_field; - } - } - return $locale; - } - /** * Add fields data to the asset data registry. */ @@ -304,16 +289,18 @@ public function get_fields() { } /** - * Returns an array of fields for a given group. - * - * @param string $location The location to get fields for (address|contact|additional). + * Update the default locale with additional fields without country limitations. * - * @return array An array of fields. + * @param array $locale The locale to update. + * @return mixed */ - public function get_fields_for_location( $location ) { - if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) { - return $this->fields_locations[ $location ]; + public function update_default_locale_with_fields( $locale ) { + foreach ( $this->fields_locations['address'] as $field_id => $additional_field ) { + if ( empty( $locale[ $field_id ] ) ) { + $locale[ $field_id ] = $additional_field; + } } + return $locale; } /** @@ -343,6 +330,19 @@ public function get_additional_fields_keys() { return $this->fields_locations['additional']; } + /** + * Returns an array of fields for a given group. + * + * @param string $location The location to get fields for (address|contact|additional). + * + * @return array An array of fields. + */ + public function get_fields_for_location( $location ) { + if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) { + return $this->fields_locations[ $location ]; + } + } + /** * Validates a field value for a given group. * @@ -395,7 +395,7 @@ public function is_field( $key ) { * * @return void */ - public function persist_field( $key, $value, $order, $set_customer = true ) { + public function persist_field_for_order( $key, $value, $order, $set_customer = true ) { $this->set_array_meta( $key, $value, $order ); if ( $set_customer ) { if ( isset( wc()->customer ) ) { @@ -403,7 +403,6 @@ public function persist_field( $key, $value, $order, $set_customer = true ) { } elseif ( $order->get_customer_id() ) { $customer = new \WC_Customer( $order->get_customer_id() ); $this->set_array_meta( $key, $value, $customer ); - $customer->save(); } } } @@ -421,82 +420,6 @@ public function persist_field_for_customer( $key, $value, $customer ) { $this->set_array_meta( $key, $value, $customer ); } - /** - * Returns a field value for a given object. - * - * @param string $key The field key. - * @param \WC_Customer $customer The customer to get the field value for. - * @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group. - * - * @return mixed The field value. - */ - public function get_field_from_customer( $key, $customer, $group = '' ) { - return $this->get_field_from_object( $key, $customer, $group ); - } - - /** - * Returns a field value for a given order. - * - * @param string $field The field key. - * @param \WC_Order $order The order to get the field value for. - * @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group. - * - * @return mixed The field value. - */ - public function get_field_from_order( $field, $order, $group = '' ) { - return $this->get_field_from_object( $field, $order, $group ); - } - - /** - * Returns an array of all fields values for a given customer. - * - * @param \WC_Customer $customer The customer to get the fields for. - * - * @return array An array of fields. - */ - public function get_all_fields_from_customer( $customer ) { - $customer_id = $customer->get_id(); - $meta_data = [ - 'billing' => [], - 'shipping' => [], - 'additional' => [], - ]; - if ( ! $customer_id ) { - if ( isset( wc()->session ) ) { - $meta_data['billing'] = wc()->session->get( self::BILLING_FIELDS_KEY, [] ); - $meta_data['shipping'] = wc()->session->get( self::SHIPPING_FIELDS_KEY, [] ); - $meta_data['additional'] = wc()->session->get( self::ADDITIONAL_FIELDS_KEY, [] ); - } - } else { - $meta_data['billing'] = get_user_meta( $customer_id, self::BILLING_FIELDS_KEY, true ); - $meta_data['shipping'] = get_user_meta( $customer_id, self::SHIPPING_FIELDS_KEY, true ); - $meta_data['additional'] = get_user_meta( $customer_id, self::ADDITIONAL_FIELDS_KEY, true ); - } - - return $this->format_meta_data( $meta_data ); - } - - /** - * Returns an array of all fields values for a given order. - * - * @param \WC_Order $order The order to get the fields for. - * - * @return array An array of fields. - */ - public function get_all_fields_from_order( $order ) { - $meta_data = [ - 'billing' => [], - 'shipping' => [], - 'additional' => [], - ]; - if ( $order instanceof \WC_Order ) { - $meta_data['billing'] = $order->get_meta( self::BILLING_FIELDS_KEY, true ); - $meta_data['shipping'] = $order->get_meta( self::SHIPPING_FIELDS_KEY, true ); - $meta_data['additional'] = $order->get_meta( self::ADDITIONAL_FIELDS_KEY, true ); - } - return $this->format_meta_data( $meta_data ); - } - /** * Sets a field value in an array meta, supporting routing things to billing, shipping, or additional fields, based on a prefix for the key. * @@ -546,6 +469,32 @@ private function set_array_meta( $key, $value, $object ) { } + /** + * Returns a field value for a given object. + * + * @param string $key The field key. + * @param \WC_Customer $customer The customer to get the field value for. + * @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group. + * + * @return mixed The field value. + */ + public function get_field_from_customer( $key, $customer, $group = '' ) { + return $this->get_field_from_object( $key, $customer, $group ); + } + + /** + * Returns a field value for a given order. + * + * @param string $field The field key. + * @param \WC_Order $order The order to get the field value for. + * @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group. + * + * @return mixed The field value. + */ + public function get_field_from_order( $field, $order, $group = '' ) { + return $this->get_field_from_object( $field, $order, $group ); + } + /** * Returns a field value for a given object. * @@ -588,6 +537,56 @@ private function get_field_from_object( $key, $object, $group = '' ) { return $meta_data[ $key ]; } + /** + * Returns an array of all fields values for a given customer. + * + * @param \WC_Customer $customer The customer to get the fields for. + * + * @return array An array of fields. + */ + public function get_all_fields_from_customer( $customer ) { + $customer_id = $customer->get_id(); + $meta_data = [ + 'billing' => [], + 'shipping' => [], + 'additional' => [], + ]; + if ( ! $customer_id ) { + if ( isset( wc()->session ) ) { + $meta_data['billing'] = wc()->session->get( self::BILLING_FIELDS_KEY, [] ); + $meta_data['shipping'] = wc()->session->get( self::SHIPPING_FIELDS_KEY, [] ); + $meta_data['additional'] = wc()->session->get( self::ADDITIONAL_FIELDS_KEY, [] ); + } + } else { + $meta_data['billing'] = get_user_meta( $customer_id, self::BILLING_FIELDS_KEY, true ); + $meta_data['shipping'] = get_user_meta( $customer_id, self::SHIPPING_FIELDS_KEY, true ); + $meta_data['additional'] = get_user_meta( $customer_id, self::ADDITIONAL_FIELDS_KEY, true ); + } + + return $this->format_meta_data( $meta_data ); + } + + /** + * Returns an array of all fields values for a given order. + * + * @param \WC_Order $order The order to get the fields for. + * + * @return array An array of fields. + */ + public function get_all_fields_from_order( $order ) { + $meta_data = [ + 'billing' => [], + 'shipping' => [], + 'additional' => [], + ]; + if ( $order instanceof \WC_Order ) { + $meta_data['billing'] = $order->get_meta( self::BILLING_FIELDS_KEY, true ); + $meta_data['shipping'] = $order->get_meta( self::SHIPPING_FIELDS_KEY, true ); + $meta_data['additional'] = $order->get_meta( self::ADDITIONAL_FIELDS_KEY, true ); + } + return $this->format_meta_data( $meta_data ); + } + /** * Returns an array of all fields values for a given meta object. It would add the billing or shipping prefix to the keys. * diff --git a/src/StoreApi/Schemas/V1/CheckoutSchema.php b/src/StoreApi/Schemas/V1/CheckoutSchema.php index 0a648c8441b..044d2054d31 100644 --- a/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -245,12 +245,16 @@ function( $key, $value ) { * @return array */ protected function get_additional_fields_response( \WC_Order $order ) { - $additional_fields_keys = array_merge( $this->additional_fields_controller->get_contact_fields_keys(), $this->additional_fields_controller->get_additional_fields_keys() ); - + $fields = $this->additional_fields_controller->get_all_fields_from_order( $order ); $response = []; - foreach ( $additional_fields_keys as $key ) { - $response[ $key ] = $this->additional_fields_controller->get_field_from_order( $key, $order ); + + foreach ( $fields as $key => $value ) { + if ( 0 === strpos( $key, '/billing/' ) || 0 === strpos( $key, '/shipping/' ) ) { + continue; + } + $response[ $key ] = $value; } + return $response; } diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php index 15d24142201..f43080b453a 100644 --- a/src/StoreApi/Utilities/CheckoutTrait.php +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -199,7 +199,7 @@ private function persist_additional_fields_for_order( \WP_REST_Request $request $errors[] = $e->getMessage(); continue; } - $this->additional_fields_controller->persist_field( $key, $value, $this->order, true ); + $this->additional_fields_controller->persist_field_for_order( $key, $value, $this->order, true ); } if ( $errors->has_errors() ) { diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index b2f52dfb4b2..c639d025284 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -739,7 +739,7 @@ protected function update_addresses_from_cart( \WC_Order $order ) { ); $customer_fields = $this->additional_fields_controller->get_all_fields_from_customer( wc()->customer ); foreach ( $customer_fields as $key => $value ) { - $this->additional_fields_controller->persist_field( $key, $value, $order, false ); + $this->additional_fields_controller->persist_field_for_order( $key, $value, $order, false ); } } } From 8ccaee8aa3a5a650684dd2938635755b726c3401 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 13:11:03 +0100 Subject: [PATCH 28/46] rename get_fields --- src/Domain/Services/CheckoutFields.php | 17 +++++++++++++---- .../Schemas/V1/AbstractAddressSchema.php | 2 +- src/StoreApi/Schemas/V1/CheckoutSchema.php | 2 +- src/StoreApi/Utilities/OrderController.php | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index d20ecac3760..4894428455c 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -223,7 +223,7 @@ public function init() { * Add fields data to the asset data registry. */ public function add_fields_data() { - $this->asset_data_registry->add( 'defaultFields', $this->get_fields(), true ); + $this->asset_data_registry->add( 'defaultFields', array_merge( $this->get_core_fields(), $this->get_additional_fields() ), true ); $this->asset_data_registry->add( 'addressFieldsLocations', $this->fields_locations, true ); } @@ -280,12 +280,21 @@ public function register_checkout_field( $options ) { } /** - * Returns an array of all fields. + * Returns an array of all core fields. * * @return array An array of fields. */ - public function get_fields() { - return array_merge( $this->core_fields, $this->additional_fields ); + public function get_core_fields() { + return $this->core_fields; + } + + /** + * Returns an array of all additional fields. + * + * @return array An array of fields. + */ + public function get_additional_fields() { + return $this->additional_fields; } /** diff --git a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index 2346ba1dbff..ab131d0af1f 100644 --- a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -178,7 +178,7 @@ public function validate_callback( $address, $request, $param ) { protected function get_additional_address_fields_schema() { $additional_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); - $fields = $this->additional_fields_controller->get_fields(); + $fields = array_merge( $this->additional_fields_controller->get_core_fields(), $this->additional_fields_controller->get_additional_fields() ); $address_fields = array_filter( $fields, diff --git a/src/StoreApi/Schemas/V1/CheckoutSchema.php b/src/StoreApi/Schemas/V1/CheckoutSchema.php index 044d2054d31..0c4a16a9ade 100644 --- a/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -266,7 +266,7 @@ protected function get_additional_fields_response( \WC_Order $order ) { protected function get_additional_fields_schema() { $additional_fields_keys = $this->additional_fields_controller->get_additional_fields_keys(); - $fields = $this->additional_fields_controller->get_fields(); + $fields = $this->additional_fields_controller->get_additional_fields(); $additional_fields = array_filter( $fields, diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index c639d025284..82aac3485ea 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -393,7 +393,7 @@ protected function validate_address_fields( \WC_Order $order, $address_type, \WP } } - $fields = $this->additional_fields_controller->get_fields(); + $fields = $this->additional_fields_controller->get_additional_fields(); $address_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); $address_fields = array_filter( $fields, From 3a02c06a2180c95431564ff05e5b5442349cc177 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 13:56:44 +0100 Subject: [PATCH 29/46] handle review comments --- src/Domain/Bootstrap.php | 2 +- src/Domain/Services/CheckoutFields.php | 10 ++++----- src/StoreApi/Routes/V1/CartUpdateCustomer.php | 2 +- .../Schemas/V1/AbstractAddressSchema.php | 21 ++++++++++++------- src/StoreApi/StoreApi.php | 13 +----------- src/StoreApi/Utilities/CheckoutTrait.php | 2 +- 6 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/Domain/Bootstrap.php b/src/Domain/Bootstrap.php index baab522c8eb..0e10312de84 100644 --- a/src/Domain/Bootstrap.php +++ b/src/Domain/Bootstrap.php @@ -126,7 +126,7 @@ function() { $is_store_api_request = $is_rest && ! empty( $_SERVER['REQUEST_URI'] ) && ( false !== strpos( $_SERVER['REQUEST_URI'], trailingslashit( rest_get_url_prefix() ) . 'wc/store/' ) ); // Load and init assets. - $this->container->get( StoreApi::class )->init( $this->container->get( CheckoutFields::class ) ); + $this->container->get( StoreApi::class )->init(); $this->container->get( PaymentsApi::class )->init(); $this->container->get( DraftOrders::class )->init(); $this->container->get( CreateAccount::class )->init(); diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 4894428455c..358586bc564 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -269,11 +269,11 @@ public function register_checkout_field( $options ) { // Insert new field into the correct location array. $this->additional_fields[ $id ] = array( 'label' => $options['label'], - 'optionalLabel' => ! empty( $options['optionalLabel'] ) ? $options['optionalLabel'] : '', - 'required' => ! empty( $options['required'] ) ? $options['required'] : false, - 'hidden' => ! empty( $options['hidden'] ) ? $options['hidden'] : false, - 'autocomplete' => ! empty( $options['autocomplete'] ) ? $options['autocomplete'] : '', - 'autocapitalize' => ! empty( $options['autocapitalize'] ) ? $options['autocapitalize'] : '', + 'optionalLabel' => $options['optionalLabel'] ?: '', + 'required' => $options['required'] ?: false, + 'hidden' => $options['hidden'] ?: false, + 'autocomplete' => $options['autocomplete'] ?: '', + 'autocapitalize' => $options['autocapitalize'] ?: '', ); $this->fields_locations[ $location ][] = $id; diff --git a/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/src/StoreApi/Routes/V1/CartUpdateCustomer.php index ad48dce4f05..0b5a9e5662c 100644 --- a/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -183,7 +183,7 @@ protected function get_route_post_response( \WP_REST_Request $request ) { ) ); // We want to only get additional fields passed, since core ones are already saved. - $core_fields = array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'phone', 'email' ); + $core_fields = array_keys( $this->additional_fields_controller->get_core_fields() ); $additional_shipping_values = array_diff_key( $shipping, array_flip( $core_fields ) ); $additional_billing_values = array_diff_key( $billing, array_flip( $core_fields ) ); diff --git a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index ab131d0af1f..87dc556cef1 100644 --- a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -97,14 +97,19 @@ public function sanitize_callback( $address, $request, $param ) { $address = array_reduce( array_keys( $address ), function( $carry, $key ) use ( $address, $validation_util ) { - if ( 'country' === $key ) { - $carry[ $key ] = wc_strtoupper( sanitize_text_field( wp_unslash( $address[ $key ] ) ) ); - } elseif ( 'state' === $key ) { - $carry[ $key ] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address[ $key ] ) ), $address['country'] ); - } elseif ( 'postcode' === $key ) { - $carry[ $key ] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; - } else { - $carry[ $key ] = sanitize_text_field( wp_unslash( $address[ $key ] ) ); + switch ( $key ) { + case 'country': + $carry[ $key ] = wc_strtoupper( sanitize_text_field( wp_unslash( $address[ $key ] ) ) ); + break; + case 'state': + $carry[ $key ] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address[ $key ] ) ), $address['country'] ); + break; + case 'postcode': + $carry[ $key ] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : ''; + break; + default: + $carry[ $key ] = sanitize_text_field( wp_unslash( $address[ $key ] ) ); + break; } return $carry; }, diff --git a/src/StoreApi/StoreApi.php b/src/StoreApi/StoreApi.php index 80f4250287f..7b740e37a2e 100644 --- a/src/StoreApi/StoreApi.php +++ b/src/StoreApi/StoreApi.php @@ -1,7 +1,6 @@ register( - CheckoutFields::class, - function () use ( $checkout_fields ) { - return $checkout_fields; - } - ); - + public function init() { add_action( 'rest_api_init', function() { diff --git a/src/StoreApi/Utilities/CheckoutTrait.php b/src/StoreApi/Utilities/CheckoutTrait.php index f43080b453a..2411f47e938 100644 --- a/src/StoreApi/Utilities/CheckoutTrait.php +++ b/src/StoreApi/Utilities/CheckoutTrait.php @@ -199,7 +199,7 @@ private function persist_additional_fields_for_order( \WP_REST_Request $request $errors[] = $e->getMessage(); continue; } - $this->additional_fields_controller->persist_field_for_order( $key, $value, $this->order, true ); + $this->additional_fields_controller->persist_field_for_order( $key, $value, $this->order, false ); } if ( $errors->has_errors() ) { From 6c14f15b7f1a5f8ee62bd0a49bc278231c8bef58 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 13:59:48 +0100 Subject: [PATCH 30/46] fix rebase error --- assets/js/base/utils/address.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/assets/js/base/utils/address.ts b/assets/js/base/utils/address.ts index 92c400cb6ac..dc016722ef8 100644 --- a/assets/js/base/utils/address.ts +++ b/assets/js/base/utils/address.ts @@ -156,10 +156,11 @@ export const isAddressComplete = ( if ( ! address.country ) { return false; } - const fields = Object.keys( - defaultAddressFields - ) as ( keyof AddressFields )[]; - const addressFields = prepareAddressFields( fields, {}, address.country ); + const addressFields = prepareAddressFields( + ADDRESS_FIELDS_KEYS, + {}, + address.country + ); return addressFields.every( ( { key = '', hidden = false, required = false } ) => { From 58d422b7ac423202c5a37673f4ef43a9659d177e Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 14:11:33 +0100 Subject: [PATCH 31/46] fix unit test --- assets/js/settings/shared/default-fields.ts | 127 +++++++++++++++++++- 1 file changed, 125 insertions(+), 2 deletions(-) diff --git a/assets/js/settings/shared/default-fields.ts b/assets/js/settings/shared/default-fields.ts index 2a547f3760a..f91fbc6330b 100644 --- a/assets/js/settings/shared/default-fields.ts +++ b/assets/js/settings/shared/default-fields.ts @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ @@ -67,10 +72,128 @@ export interface BillingAddress extends ShippingAddress { } export type CountryAddressFields = Record< string, AddressFields >; +export const defaultFieldsDefinition: AddressFields = { + first_name: { + label: __( 'First name', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'First name (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'given-name', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 10, + }, + last_name: { + label: __( 'Last name', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'Last name (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'family-name', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 20, + }, + company: { + label: __( 'Company', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'Company (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'organization', + autocapitalize: 'sentences', + required: false, + hidden: false, + index: 30, + }, + address_1: { + label: __( 'Address', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'Address (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'address-line1', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 40, + }, + address_2: { + label: __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'Apartment, suite, etc. (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'address-line2', + autocapitalize: 'sentences', + required: false, + hidden: false, + index: 50, + }, + country: { + label: __( 'Country/Region', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'Country/Region (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'country', + required: true, + hidden: false, + index: 60, + }, + city: { + label: __( 'City', 'woo-gutenberg-products-block' ), + optionalLabel: __( 'City (optional)', 'woo-gutenberg-products-block' ), + autocomplete: 'address-level2', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 70, + }, + state: { + label: __( 'State/County', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'State/County (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'address-level1', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 80, + }, + postcode: { + label: __( 'Postal code', 'woo-gutenberg-products-block' ), + optionalLabel: __( + 'Postal code (optional)', + 'woo-gutenberg-products-block' + ), + autocomplete: 'postal-code', + autocapitalize: 'characters', + required: true, + hidden: false, + index: 90, + }, + phone: { + label: __( 'Phone', 'woo-gutenberg-products-block' ), + optionalLabel: __( 'Phone (optional)', 'woo-gutenberg-products-block' ), + autocomplete: 'tel', + type: 'tel', + required: true, + hidden: false, + index: 100, + }, +}; + /** * Default field properties. */ -export const defaultFields: AddressFields = - getSetting< AddressFields >( 'defaultFields' ); +export const defaultFields: AddressFields = getSetting< AddressFields >( + 'defaultFields', + defaultFieldsDefinition +); export default defaultFields; From c416753ab0ca8cb4998fac5bccbb508dbfe52d87 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 14:12:37 +0100 Subject: [PATCH 32/46] remove todo --- src/Domain/Services/CheckoutFields.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 358586bc564..de33589c22f 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -357,9 +357,7 @@ public function get_fields_for_location( $location ) { * * @param string $key The field key. * @param mixed $value The field value. - * @param string $location The location to validate the field for (address|contact|additional). - * - * TODO: we might not need the location param here. + * @param string $location The gslocation to validate the field for (address|contact|additional). * * @return true|\WP_Error True if the field is valid, a WP_Error otherwise. */ From 9d35d147dddd48919cacfd05809dd54d513fe807 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 14:17:45 +0100 Subject: [PATCH 33/46] move fields defaults to jest --- assets/js/settings/shared/default-fields.ts | 127 +------------------- tests/js/setup-globals.js | 91 ++++++++++++++ 2 files changed, 93 insertions(+), 125 deletions(-) diff --git a/assets/js/settings/shared/default-fields.ts b/assets/js/settings/shared/default-fields.ts index f91fbc6330b..2a547f3760a 100644 --- a/assets/js/settings/shared/default-fields.ts +++ b/assets/js/settings/shared/default-fields.ts @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ @@ -72,128 +67,10 @@ export interface BillingAddress extends ShippingAddress { } export type CountryAddressFields = Record< string, AddressFields >; -export const defaultFieldsDefinition: AddressFields = { - first_name: { - label: __( 'First name', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'First name (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'given-name', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 10, - }, - last_name: { - label: __( 'Last name', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Last name (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'family-name', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 20, - }, - company: { - label: __( 'Company', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Company (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'organization', - autocapitalize: 'sentences', - required: false, - hidden: false, - index: 30, - }, - address_1: { - label: __( 'Address', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Address (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'address-line1', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 40, - }, - address_2: { - label: __( 'Apartment, suite, etc.', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Apartment, suite, etc. (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'address-line2', - autocapitalize: 'sentences', - required: false, - hidden: false, - index: 50, - }, - country: { - label: __( 'Country/Region', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Country/Region (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'country', - required: true, - hidden: false, - index: 60, - }, - city: { - label: __( 'City', 'woo-gutenberg-products-block' ), - optionalLabel: __( 'City (optional)', 'woo-gutenberg-products-block' ), - autocomplete: 'address-level2', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 70, - }, - state: { - label: __( 'State/County', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'State/County (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'address-level1', - autocapitalize: 'sentences', - required: true, - hidden: false, - index: 80, - }, - postcode: { - label: __( 'Postal code', 'woo-gutenberg-products-block' ), - optionalLabel: __( - 'Postal code (optional)', - 'woo-gutenberg-products-block' - ), - autocomplete: 'postal-code', - autocapitalize: 'characters', - required: true, - hidden: false, - index: 90, - }, - phone: { - label: __( 'Phone', 'woo-gutenberg-products-block' ), - optionalLabel: __( 'Phone (optional)', 'woo-gutenberg-products-block' ), - autocomplete: 'tel', - type: 'tel', - required: true, - hidden: false, - index: 100, - }, -}; - /** * Default field properties. */ -export const defaultFields: AddressFields = getSetting< AddressFields >( - 'defaultFields', - defaultFieldsDefinition -); +export const defaultFields: AddressFields = + getSetting< AddressFields >( 'defaultFields' ); export default defaultFields; diff --git a/tests/js/setup-globals.js b/tests/js/setup-globals.js index dc246e79e9e..f5aa5c169a3 100644 --- a/tests/js/setup-globals.js +++ b/tests/js/setup-globals.js @@ -117,6 +117,97 @@ global.wcSettings = { attribute_public: 0, }, ], + defaultFields: { + first_name: { + label: 'First name', + optionalLabel: 'First name (optional)', + autocomplete: 'given-name', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 10, + }, + last_name: { + label: 'Last name', + optionalLabel: 'Last name (optional)', + autocomplete: 'family-name', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 20, + }, + company: { + label: 'Company', + optionalLabel: 'Company (optional)', + autocomplete: 'organization', + autocapitalize: 'sentences', + required: false, + hidden: false, + index: 30, + }, + address_1: { + label: 'Address', + optionalLabel: 'Address (optional)', + autocomplete: 'address-line1', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 40, + }, + address_2: { + label: 'Apartment, suite, etc.', + optionalLabel: 'Apartment, suite, etc. (optional)', + autocomplete: 'address-line2', + autocapitalize: 'sentences', + required: false, + hidden: false, + index: 50, + }, + country: { + label: 'Country/Region', + optionalLabel: 'Country/Region (optional)', + autocomplete: 'country', + required: true, + hidden: false, + index: 60, + }, + city: { + label: 'City', + optionalLabel: 'City (optional)', + autocomplete: 'address-level2', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 70, + }, + state: { + label: 'State/County', + optionalLabel: 'State/County (optional)', + autocomplete: 'address-level1', + autocapitalize: 'sentences', + required: true, + hidden: false, + index: 80, + }, + postcode: { + label: 'Postal code', + optionalLabel: 'Postal code (optional)', + autocomplete: 'postal-code', + autocapitalize: 'characters', + required: true, + hidden: false, + index: 90, + }, + phone: { + label: 'Phone', + optionalLabel: 'Phone (optional)', + autocomplete: 'tel', + type: 'tel', + required: true, + hidden: false, + index: 100, + }, + }, }; global.jQuery = () => ( { From 856cf89cf3efbd3899a99588e7d1f7edc35bdfdc Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 8 Dec 2023 13:40:17 +0000 Subject: [PATCH 34/46] Ensure shipping address includes additional fields --- .../Schemas/V1/ShippingAddressSchema.php | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/StoreApi/Schemas/V1/ShippingAddressSchema.php b/src/StoreApi/Schemas/V1/ShippingAddressSchema.php index 1461d3e0fad..f28f734cae3 100644 --- a/src/StoreApi/Schemas/V1/ShippingAddressSchema.php +++ b/src/StoreApi/Schemas/V1/ShippingAddressSchema.php @@ -63,23 +63,31 @@ function( $carry, $key ) use ( $additional_address_fields ) { [] ); - return $this->prepare_html_response( - array_merge( - [ - 'first_name' => $address->get_shipping_first_name(), - 'last_name' => $address->get_shipping_last_name(), - 'company' => $address->get_shipping_company(), - 'address_1' => $address->get_shipping_address_1(), - 'address_2' => $address->get_shipping_address_2(), - 'city' => $address->get_shipping_city(), - 'state' => $shipping_state, - 'postcode' => $address->get_shipping_postcode(), - 'country' => $shipping_country, - 'phone' => $address->get_shipping_phone(), - ], - $additional_address_fields - ) + $address_object = array_merge( + [ + 'first_name' => $address->get_shipping_first_name(), + 'last_name' => $address->get_shipping_last_name(), + 'company' => $address->get_shipping_company(), + 'address_1' => $address->get_shipping_address_1(), + 'address_2' => $address->get_shipping_address_2(), + 'city' => $address->get_shipping_city(), + 'state' => $shipping_state, + 'postcode' => $address->get_shipping_postcode(), + 'country' => $shipping_country, + 'phone' => $address->get_shipping_phone(), + ], + $additional_address_fields ); + + // Add any missing keys from additional_fields_controller to the address response. + foreach ( $this->additional_fields_controller->get_address_fields_keys() as $field ) { + if ( isset( $address_object[ $field ] ) ) { + continue; + } + $address_object[ $field ] = ''; + } + + return $this->prepare_html_response( $address_object ); } throw new RouteException( From 6361137f6740957aa6429b5c621cc64c6ef793b8 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 8 Dec 2023 13:40:28 +0000 Subject: [PATCH 35/46] Ensure billing address includes additional fields --- .../Schemas/V1/BillingAddressSchema.php | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/StoreApi/Schemas/V1/BillingAddressSchema.php b/src/StoreApi/Schemas/V1/BillingAddressSchema.php index 68fb3b8bff6..ea08933b60d 100644 --- a/src/StoreApi/Schemas/V1/BillingAddressSchema.php +++ b/src/StoreApi/Schemas/V1/BillingAddressSchema.php @@ -120,24 +120,32 @@ function( $carry, $key ) use ( $additional_address_fields ) { [] ); - return $this->prepare_html_response( - \array_merge( - [ - 'first_name' => $address->get_billing_first_name(), - 'last_name' => $address->get_billing_last_name(), - 'company' => $address->get_billing_company(), - 'address_1' => $address->get_billing_address_1(), - 'address_2' => $address->get_billing_address_2(), - 'city' => $address->get_billing_city(), - 'state' => $billing_state, - 'postcode' => $address->get_billing_postcode(), - 'country' => $billing_country, - 'email' => $address->get_billing_email(), - 'phone' => $address->get_billing_phone(), - ], - $additional_address_fields - ) + $address_object = \array_merge( + [ + 'first_name' => $address->get_billing_first_name(), + 'last_name' => $address->get_billing_last_name(), + 'company' => $address->get_billing_company(), + 'address_1' => $address->get_billing_address_1(), + 'address_2' => $address->get_billing_address_2(), + 'city' => $address->get_billing_city(), + 'state' => $billing_state, + 'postcode' => $address->get_billing_postcode(), + 'country' => $billing_country, + 'email' => $address->get_billing_email(), + 'phone' => $address->get_billing_phone(), + ], + $additional_address_fields ); + + // Add any missing keys from additional_fields_controller to the address response. + foreach ( $this->additional_fields_controller->get_address_fields_keys() as $field ) { + if ( isset( $address_object[ $field ] ) ) { + continue; + } + $address_object[ $field ] = ''; + } + + return $this->prepare_html_response( $address_object ); } throw new RouteException( 'invalid_object_type', From 36419770e2185e7932733f216d00336dea781733 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 8 Dec 2023 13:41:43 +0000 Subject: [PATCH 36/46] Ensure default cart state includes additional fields --- assets/js/data/cart/default-state.ts | 54 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/assets/js/data/cart/default-state.ts b/assets/js/data/cart/default-state.ts index 8ce98387a77..fd6a3d0d3d2 100644 --- a/assets/js/data/cart/default-state.ts +++ b/assets/js/data/cart/default-state.ts @@ -1,7 +1,14 @@ /** * External dependencies */ -import type { Cart, CartMeta, ApiErrorResponse } from '@woocommerce/types'; +import type { + Cart, + CartMeta, + ApiErrorResponse, + CartShippingAddress, + CartBillingAddress, +} from '@woocommerce/types'; +import { AddressFields, getSetting } from '@woocommerce/settings'; /** * Internal dependencies @@ -30,37 +37,32 @@ export interface CartState { metaData: CartMeta; errors: ApiErrorResponse[]; } + +/** + * Default field properties. + */ +export const defaultFields: AddressFields = + getSetting< AddressFields >( 'defaultFields' ); + +const shippingAddress: Partial< CartShippingAddress > = {}; +const billingAddress: Partial< CartBillingAddress > = {}; + +Object.keys( defaultFields ).forEach( ( key ) => { + shippingAddress[ key ] = ''; +} ); + +Object.keys( defaultFields ).forEach( ( key ) => { + billingAddress[ key ] = ''; +} ); + export const defaultCartState: CartState = { cartItemsPendingQuantity: EMPTY_PENDING_QUANTITY, cartItemsPendingDelete: EMPTY_PENDING_DELETE, cartData: { coupons: EMPTY_CART_COUPONS, shippingRates: EMPTY_SHIPPING_RATES, - shippingAddress: { - first_name: '', - last_name: '', - company: '', - address_1: '', - address_2: '', - city: '', - state: '', - postcode: '', - country: '', - phone: '', - }, - billingAddress: { - first_name: '', - last_name: '', - company: '', - address_1: '', - address_2: '', - city: '', - state: '', - postcode: '', - country: '', - phone: '', - email: '', - }, + shippingAddress: shippingAddress as CartShippingAddress, + billingAddress: billingAddress as CartBillingAddress, items: EMPTY_CART_ITEMS, itemsCount: 0, itemsWeight: 0, From 0fdfd6c20805e7360db8694d29374fe5e8571a44 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 8 Dec 2023 13:42:35 +0000 Subject: [PATCH 37/46] Revert "Ensure fields are updated when changing custom value for first time" This reverts commit 36f14603bac23a07b81ae4de1383072d636e2af1. --- assets/js/data/cart/utils.ts | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/assets/js/data/cart/utils.ts b/assets/js/data/cart/utils.ts index 9a7b69376b5..40d0aef5096 100644 --- a/assets/js/data/cart/utils.ts +++ b/assets/js/data/cart/utils.ts @@ -45,8 +45,7 @@ export const shippingAddressHasValidationErrors = () => { export type BaseAddressKey = | keyof CartBillingAddress - | keyof CartShippingAddress - | string; // string here because custom checkout fields can be added with arbitrary keys. + | keyof CartShippingAddress; /** * Normalizes address values before push. @@ -83,23 +82,12 @@ export const getDirtyKeys = < previousAddress ) as BaseAddressKey[]; - const addedKeys = Object.keys( address ).filter( - ( key ) => ! previousAddressKeys.includes( key ) - ); - - const removedKeys = previousAddressKeys.filter( - ( key ) => ! Object.keys( address ).includes( key ) - ); - - return previousAddressKeys - .filter( ( key: BaseAddressKey ) => { - return ( - normalizeAddressProp( key, previousAddress[ key ] ) !== - normalizeAddressProp( key, address[ key ] ) - ); - } ) - .concat( removedKeys ) - .concat( addedKeys ); + return previousAddressKeys.filter( ( key: BaseAddressKey ) => { + return ( + normalizeAddressProp( key, previousAddress[ key ] ) !== + normalizeAddressProp( key, address[ key ] ) + ); + } ); }; /** From db497dcc31235eacc8c16d9543a86574a7ab0f73 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Fri, 8 Dec 2023 15:33:32 +0100 Subject: [PATCH 38/46] fix default value for additional_fields key --- src/Domain/Services/CheckoutFields.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index de33589c22f..7fd990534b5 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -23,7 +23,7 @@ class CheckoutFields { * * @var array */ - private $additional_fields; + private $additional_fields = []; /** * Fields locations. From 70bf9efe068cbf5e448ee299cfffe880fffe90fc Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Fri, 8 Dec 2023 14:48:51 +0000 Subject: [PATCH 39/46] Use default fields from constants and dont get setting again --- assets/js/data/cart/default-state.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/assets/js/data/cart/default-state.ts b/assets/js/data/cart/default-state.ts index fd6a3d0d3d2..dc1f0e55e12 100644 --- a/assets/js/data/cart/default-state.ts +++ b/assets/js/data/cart/default-state.ts @@ -8,7 +8,7 @@ import type { CartShippingAddress, CartBillingAddress, } from '@woocommerce/types'; -import { AddressFields, getSetting } from '@woocommerce/settings'; +import { AddressField, defaultFields } from '@woocommerce/settings'; /** * Internal dependencies @@ -38,20 +38,21 @@ export interface CartState { errors: ApiErrorResponse[]; } -/** - * Default field properties. - */ -export const defaultFields: AddressFields = - getSetting< AddressFields >( 'defaultFields' ); - -const shippingAddress: Partial< CartShippingAddress > = {}; -const billingAddress: Partial< CartBillingAddress > = {}; - +const shippingAddress: Partial< + CartShippingAddress & { email: AddressField } +> = {}; Object.keys( defaultFields ).forEach( ( key ) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore the default fields contain keys for each field. shippingAddress[ key ] = ''; } ); +delete shippingAddress.email; +const billingAddress: Partial< CartBillingAddress & { email: AddressField } > = + {}; Object.keys( defaultFields ).forEach( ( key ) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore the default fields contain keys for each field. billingAddress[ key ] = ''; } ); From 5ada34026c6b2e20bd3370034a36c5c9c5d8f9f6 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 11 Dec 2023 12:45:01 +0000 Subject: [PATCH 40/46] Ensure values have defaults if the config doesn't pass them --- src/Domain/Services/CheckoutFields.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 7fd990534b5..83971855b2a 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -269,11 +269,10 @@ public function register_checkout_field( $options ) { // Insert new field into the correct location array. $this->additional_fields[ $id ] = array( 'label' => $options['label'], - 'optionalLabel' => $options['optionalLabel'] ?: '', - 'required' => $options['required'] ?: false, - 'hidden' => $options['hidden'] ?: false, - 'autocomplete' => $options['autocomplete'] ?: '', - 'autocapitalize' => $options['autocapitalize'] ?: '', + 'optionalLabel' => empty( $options['optionalLabel'] ) ? '' : $options['optionalLabel'], + 'required' => empty( $options['required'] ) ? false : $options['required'], + 'autocomplete' => empty( $options['autocomplete'] ) ? '' : $options['autocomplete'], + 'autocapitalize' => empty( $options['autocapitalize'] ) ? '' : $options['autocapitalize'], ); $this->fields_locations[ $location ][] = $id; From 0ab9b3ff0f48ed45460e38fe517740ab0d77d23d Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 11 Dec 2023 12:51:49 +0000 Subject: [PATCH 41/46] Show warning if a field is registered as hidden --- src/Domain/Services/CheckoutFields.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 83971855b2a..13df2f13d21 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -266,9 +266,16 @@ public function register_checkout_field( $options ) { return new \WP_Error( 'woocommerce_blocks_checkout_field_already_registered', __( 'The field is already registered.', 'woo-gutenberg-products-block' ) ); } + // Hidden fields are not supported right now. They will be registered with hidden => false. + if ( ! empty( $options['hidden'] ) && true === $options['hidden'] ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', esc_html( $id ) ), E_USER_WARNING ); + } + // Insert new field into the correct location array. $this->additional_fields[ $id ] = array( 'label' => $options['label'], + 'hidden' => false, 'optionalLabel' => empty( $options['optionalLabel'] ) ? '' : $options['optionalLabel'], 'required' => empty( $options['required'] ) ? false : $options['required'], 'autocomplete' => empty( $options['autocomplete'] ) ? '' : $options['autocomplete'], From 704ce908852f902df5629c7637d6e90bb4b44b89 Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 11 Dec 2023 13:00:13 +0000 Subject: [PATCH 42/46] Fix lint error --- .../checkout-billing-address-block/customer-address.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx index 5127012638d..2c367eb8e63 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/customer-address.tsx @@ -102,11 +102,7 @@ const CustomerAddress = ( { /> ), - [ - addressFieldsConfig, - billingAddress, - onChangeAddress, - ] + [ addressFieldsConfig, billingAddress, onChangeAddress ] ); return ( From 9d25427146deb4eee0e7b4bb83e73f87113275aa Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 11 Dec 2023 14:48:22 +0000 Subject: [PATCH 43/46] Ensure register field runs only when woocommerce_blocks_loaded fired --- src/Domain/Services/functions.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Domain/Services/functions.php b/src/Domain/Services/functions.php index 1ae1ae3df5e..dfa5c9cb099 100644 --- a/src/Domain/Services/functions.php +++ b/src/Domain/Services/functions.php @@ -11,6 +11,19 @@ * @throws Exception If field registration fails. */ function woocommerce_blocks_register_checkout_field( $options ) { + + // Check if `woocommerce_blocks_loaded` ran. If not then the CheckoutFields class will not be available yet. + // In that case, re-hook `woocommerce_blocks_loaded` and try running this again. + $woocommerce_blocks_loaded_ran = did_action( 'woocommerce_blocks_loaded' ); + if ( ! $woocommerce_blocks_loaded_ran ) { + add_action( + 'woocommerce_blocks_loaded', + function() use ( $options ) { + woocommerce_blocks_register_checkout_field( $options ); + } + ); + return; + } $checkout_fields = Package::container()->get( \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields::class ); $result = $checkout_fields->register_checkout_field( $options ); if ( is_wp_error( $result ) ) { From 53694ad707b50597735fc88c27a01e405a32ab76 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Mon, 11 Dec 2023 16:58:36 +0100 Subject: [PATCH 44/46] only return registred fields in Store API schema --- src/Domain/Services/CheckoutFields.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Domain/Services/CheckoutFields.php b/src/Domain/Services/CheckoutFields.php index 13df2f13d21..a101c3b84bc 100644 --- a/src/Domain/Services/CheckoutFields.php +++ b/src/Domain/Services/CheckoutFields.php @@ -554,10 +554,11 @@ private function get_field_from_object( $key, $object, $group = '' ) { * Returns an array of all fields values for a given customer. * * @param \WC_Customer $customer The customer to get the fields for. + * @param bool $all Whether to return all fields or only the ones that are still registered. Default false. * * @return array An array of fields. */ - public function get_all_fields_from_customer( $customer ) { + public function get_all_fields_from_customer( $customer, $all = false ) { $customer_id = $customer->get_id(); $meta_data = [ 'billing' => [], @@ -576,17 +577,18 @@ public function get_all_fields_from_customer( $customer ) { $meta_data['additional'] = get_user_meta( $customer_id, self::ADDITIONAL_FIELDS_KEY, true ); } - return $this->format_meta_data( $meta_data ); + return $this->format_meta_data( $meta_data, $all ); } /** * Returns an array of all fields values for a given order. * * @param \WC_Order $order The order to get the fields for. + * @param bool $all Whether to return all fields or only the ones that are still registered. Default false. * * @return array An array of fields. */ - public function get_all_fields_from_order( $order ) { + public function get_all_fields_from_order( $order, $all = false ) { $meta_data = [ 'billing' => [], 'shipping' => [], @@ -597,17 +599,18 @@ public function get_all_fields_from_order( $order ) { $meta_data['shipping'] = $order->get_meta( self::SHIPPING_FIELDS_KEY, true ); $meta_data['additional'] = $order->get_meta( self::ADDITIONAL_FIELDS_KEY, true ); } - return $this->format_meta_data( $meta_data ); + return $this->format_meta_data( $meta_data, $all ); } /** * Returns an array of all fields values for a given meta object. It would add the billing or shipping prefix to the keys. * * @param array $meta The meta data to format. + * @param bool $all Whether to return all fields or only the ones that are still registered. Default false. * * @return array An array of fields. */ - private function format_meta_data( $meta ) { + private function format_meta_data( $meta, $all = false ) { $billing_fields = $meta['billing'] ?? []; $shipping_fields = $meta['shipping'] ?? []; $additional_fields = $meta['additional'] ?? []; @@ -616,18 +619,27 @@ private function format_meta_data( $meta ) { if ( is_array( $billing_fields ) ) { foreach ( $billing_fields as $key => $value ) { + if ( ! $all && ! $this->is_field( $key ) ) { + continue; + } $fields[ '/billing/' . $key ] = $value; } } if ( is_array( $shipping_fields ) ) { foreach ( $shipping_fields as $key => $value ) { + if ( ! $all && ! $this->is_field( $key ) ) { + continue; + } $fields[ '/shipping/' . $key ] = $value; } } if ( is_array( $additional_fields ) ) { foreach ( $additional_fields as $key => $value ) { + if ( ! $all && ! $this->is_field( $key ) ) { + continue; + } $fields[ $key ] = $value; } } From f8567fb713093248daeb4636d7b7d28f093a9ba4 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Mon, 11 Dec 2023 18:07:44 +0100 Subject: [PATCH 45/46] move class calling to be inside classes --- src/StoreApi/Routes/V1/AbstractCartRoute.php | 18 +++++++++++---- src/StoreApi/Routes/V1/AbstractRoute.php | 12 ++-------- src/StoreApi/Routes/V1/Checkout.php | 3 +++ src/StoreApi/Routes/V1/Order.php | 3 +-- .../Schemas/V1/AbstractAddressSchema.php | 23 ++++++++++++++++++- src/StoreApi/Schemas/V1/AbstractSchema.php | 11 ++------- src/StoreApi/Schemas/V1/CheckoutSchema.php | 17 ++++++++++---- src/StoreApi/Schemas/V1/OrderSchema.php | 2 +- src/StoreApi/Utilities/OrderController.php | 7 +++--- 9 files changed, 61 insertions(+), 35 deletions(-) diff --git a/src/StoreApi/Routes/V1/AbstractCartRoute.php b/src/StoreApi/Routes/V1/AbstractCartRoute.php index 2f40c6603fd..135fefe7ff3 100644 --- a/src/StoreApi/Routes/V1/AbstractCartRoute.php +++ b/src/StoreApi/Routes/V1/AbstractCartRoute.php @@ -2,6 +2,8 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1; +use Automattic\WooCommerce\Blocks\Package; +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema; @@ -61,6 +63,13 @@ abstract class AbstractCartRoute extends AbstractRoute { */ protected $order_controller; + /** + * Additional fields controller class instance. + * + * @var CheckoutFields + */ + protected $additional_fields_controller; + /** * Constructor. * @@ -70,10 +79,11 @@ abstract class AbstractCartRoute extends AbstractRoute { public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) { parent::__construct( $schema_controller, $schema ); - $this->cart_schema = $this->schema_controller->get( CartSchema::IDENTIFIER ); - $this->cart_item_schema = $this->schema_controller->get( CartItemSchema::IDENTIFIER ); - $this->cart_controller = new CartController(); - $this->order_controller = new OrderController( $this->additional_fields_controller ); + $this->cart_schema = $this->schema_controller->get( CartSchema::IDENTIFIER ); + $this->cart_item_schema = $this->schema_controller->get( CartItemSchema::IDENTIFIER ); + $this->cart_controller = new CartController(); + $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); + $this->order_controller = new OrderController(); } /** diff --git a/src/StoreApi/Routes/V1/AbstractRoute.php b/src/StoreApi/Routes/V1/AbstractRoute.php index d397702e70f..0f8bb8d9d1b 100644 --- a/src/StoreApi/Routes/V1/AbstractRoute.php +++ b/src/StoreApi/Routes/V1/AbstractRoute.php @@ -35,13 +35,6 @@ abstract class AbstractRoute implements RouteInterface { */ protected $schema_controller; - /** - * Checkout fields controller. - * - * @var CheckoutFields - */ - protected CheckoutFields $additional_fields_controller; - /** * The routes schema. * @@ -63,9 +56,8 @@ abstract class AbstractRoute implements RouteInterface { * @param AbstractSchema $schema Schema class for this route. */ public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) { - $this->schema_controller = $schema_controller; - $this->schema = $schema; - $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); + $this->schema_controller = $schema_controller; + $this->schema = $schema; } /** diff --git a/src/StoreApi/Routes/V1/Checkout.php b/src/StoreApi/Routes/V1/Checkout.php index fd91b18b8df..51c716c2dd7 100644 --- a/src/StoreApi/Routes/V1/Checkout.php +++ b/src/StoreApi/Routes/V1/Checkout.php @@ -10,6 +10,9 @@ use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException; use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait; +use Automattic\WooCommerce\StoreApi\SchemaController; +use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema; +use Automattic\WooCommerce\Blocks\Package; /** * Checkout class. diff --git a/src/StoreApi/Routes/V1/Order.php b/src/StoreApi/Routes/V1/Order.php index 3c8e8592d23..dd39e3c393d 100644 --- a/src/StoreApi/Routes/V1/Order.php +++ b/src/StoreApi/Routes/V1/Order.php @@ -42,8 +42,7 @@ class Order extends AbstractRoute { */ public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) { parent::__construct( $schema_controller, $schema ); - - $this->order_controller = new OrderController( $this->additional_fields_controller ); + $this->order_controller = new OrderController(); } /** diff --git a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index 87dc556cef1..cd01eea160e 100644 --- a/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -2,13 +2,34 @@ namespace Automattic\WooCommerce\StoreApi\Schemas\V1; use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils; - +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; +use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; +use Automattic\WooCommerce\StoreApi\SchemaController; +use Automattic\WooCommerce\Blocks\Package; /** * AddressSchema class. * * Provides a generic address schema for composition in other schemas. */ abstract class AbstractAddressSchema extends AbstractSchema { + + /** + * Additional fields controller. + * + * @var CheckoutFields + */ + protected $additional_fields_controller; + + /** + * Constructor. + * + * @param ExtendSchema $extend ExtendSchema instance. + * @param SchemaController $controller Schema Controller instance. + */ + public function __construct( ExtendSchema $extend, SchemaController $controller ) { + parent::__construct( $extend, $controller ); + $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); + } /** * Term properties. * diff --git a/src/StoreApi/Schemas/V1/AbstractSchema.php b/src/StoreApi/Schemas/V1/AbstractSchema.php index f7fbea74b44..7d3f5ecc56f 100644 --- a/src/StoreApi/Schemas/V1/AbstractSchema.php +++ b/src/StoreApi/Schemas/V1/AbstractSchema.php @@ -33,12 +33,6 @@ abstract class AbstractSchema { */ protected $controller; - /** - * Checkout fields controller. - * - * @var CheckoutFields - */ - protected CheckoutFields $additional_fields_controller; /** * Extending key that gets added to endpoint. * @@ -53,9 +47,8 @@ abstract class AbstractSchema { * @param SchemaController $controller Schema Controller instance. */ public function __construct( ExtendSchema $extend, SchemaController $controller ) { - $this->extend = $extend; - $this->controller = $controller; - $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); + $this->extend = $extend; + $this->controller = $controller; } /** diff --git a/src/StoreApi/Schemas/V1/CheckoutSchema.php b/src/StoreApi/Schemas/V1/CheckoutSchema.php index 0c4a16a9ade..c1305ec35b7 100644 --- a/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -4,7 +4,8 @@ use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; - +use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; +use Automattic\WooCommerce\Blocks\Package; /** * CheckoutSchema class. @@ -45,6 +46,13 @@ class CheckoutSchema extends AbstractSchema { */ protected $image_attachment_schema; + /** + * Additional fields controller. + * + * @var CheckoutFields + */ + protected $additional_fields_controller; + /** * Constructor. * @@ -53,9 +61,10 @@ class CheckoutSchema extends AbstractSchema { */ public function __construct( ExtendSchema $extend, SchemaController $controller ) { parent::__construct( $extend, $controller ); - $this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER ); - $this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER ); - $this->image_attachment_schema = $this->controller->get( ImageAttachmentSchema::IDENTIFIER ); + $this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER ); + $this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER ); + $this->image_attachment_schema = $this->controller->get( ImageAttachmentSchema::IDENTIFIER ); + $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); } /** diff --git a/src/StoreApi/Schemas/V1/OrderSchema.php b/src/StoreApi/Schemas/V1/OrderSchema.php index 62bd123709f..32f090b01d7 100644 --- a/src/StoreApi/Schemas/V1/OrderSchema.php +++ b/src/StoreApi/Schemas/V1/OrderSchema.php @@ -101,7 +101,7 @@ public function __construct( ExtendSchema $extend, SchemaController $controller $this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER ); $this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER ); $this->error_schema = $this->controller->get( ErrorSchema::IDENTIFIER ); - $this->order_controller = new OrderController( $this->additional_fields_controller ); + $this->order_controller = new OrderController(); } /** diff --git a/src/StoreApi/Utilities/OrderController.php b/src/StoreApi/Utilities/OrderController.php index 82aac3485ea..dc4069f5afd 100644 --- a/src/StoreApi/Utilities/OrderController.php +++ b/src/StoreApi/Utilities/OrderController.php @@ -4,6 +4,7 @@ use \Exception; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; +use Automattic\WooCommerce\Blocks\Package; /** * OrderController class. @@ -20,11 +21,9 @@ class OrderController { /** * Constructor. - * - * @param CheckoutFields $additional_fields_controller Checkout fields controller. */ - public function __construct( CheckoutFields $additional_fields_controller ) { - $this->additional_fields_controller = $additional_fields_controller; + public function __construct() { + $this->additional_fields_controller = Package::container()->get( CheckoutFields::class ); } /** From b9aa65e6d88a6050899eb05abf57936e02788292 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 12 Dec 2023 11:28:24 +0100 Subject: [PATCH 46/46] fix tests --- tests/php/Bootstrap/MainFile.php | 18 +++++++++++++++++- tests/php/StoreApi/Routes/Cart.php | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/php/Bootstrap/MainFile.php b/tests/php/Bootstrap/MainFile.php index c450ca297c6..fe925c41c60 100644 --- a/tests/php/Bootstrap/MainFile.php +++ b/tests/php/Bootstrap/MainFile.php @@ -28,21 +28,37 @@ class MainFile extends WP_UnitTestCase { * Ensure that container is reset between tests. */ protected function setUp(): void { - // reset container + // reset container. $this->container = Package::container( true ); } + /** + * Test that the container is returned from the main file. + */ public function test_container_returns_same_instance() { $container = Package::container(); $this->assertSame( $container, $this->container ); } + /** + * Test that the container is reset when the reset flag is passed. + */ public function test_container_reset() { $container = Package::container( true ); $this->assertNotSame( $container, $this->container ); } + /** + * Asserts that the bootstrap class is returned from the container. + */ public function wc_blocks_bootstrap() { $this->assertInstanceOf( Bootstrap::class, wc_blocks_bootstrap() ); } + + /** + * Ensure that the init method is called on the bootstrap class. This is a workaround since we're using an anti-pattern for DI. + */ + protected function tearDown(): void { + Package::init(); + } } diff --git a/tests/php/StoreApi/Routes/Cart.php b/tests/php/StoreApi/Routes/Cart.php index 4c415fb400a..95cf81f1716 100644 --- a/tests/php/StoreApi/Routes/Cart.php +++ b/tests/php/StoreApi/Routes/Cart.php @@ -603,7 +603,7 @@ public function test_add_variable_product_to_cart_returns_variation_data() { array( 'variation' => array( // order matters, alphabetical attribute order. array( - 'attribute' => 'color', + 'attribute' => 'Color', 'value' => 'red', ), array(