-
Notifications
You must be signed in to change notification settings - Fork 219
Introduce Additional Fields extensibility API. #12073
base: trunk
Are you sure you want to change the base?
Conversation
The release ZIP for this PR is accessible via:
Script Dependencies ReportThere is no changed script dependency between this branch and trunk. This comment was automatically generated by the TypeScript Errors Report
assets/js/base/components/cart-checkout/totals/shipping/index.tsx
assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts assets/js/data/cart/selectors.ts |
Size Change: +4.18 kB (0%) Total Size: 1.62 MB
ℹ️ View Unchanged
|
assets/js/base/components/cart-checkout/address-form/prepare-address-fields.ts
Show resolved
Hide resolved
assets/js/data/cart/utils.ts
Outdated
); | ||
} ) | ||
.concat( removedKeys ) | ||
.concat( addedKeys ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an inline comment explaining why this is needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @opr as I'm also not sure what this mean, but it helped get additional fields picked up and pushed.
* | ||
* @var string | ||
*/ | ||
const BILLING_FIELDS_KEY = '_additional_billing_fields'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed, noting we could investigate using individual meta keys/values for new fields to avoid serialisation. So the data is queryable and searchable.
} 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 ] ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can additional fields consumers change this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently no, do you see a good use case for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If something is defining a custom property in schema, it would make sense they can handle sanitisation but I'm not 100% sure.
@@ -274,16 +296,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' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make sure this wasn't in place for a reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was only for ergonomics, we don't change billing_address
or shipping_address
values at all during the function call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One is filtered. e.g. woocommerce_get_order_address
so this might be breaking in ways I am unsure of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not sure I see your point here.
Because we need $order
in validate_address_fields
. Previously it was only getting the address, right now, we pass the order and pull from the inside, but in both cases we're using get_address
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point is $order->get_address( 'billing' )
is ran through different filters to $order->get_billing_country()
. It probably doesn't matter, it just struck me as odd we'd use $order->get_address
here instead of the dedicated function unless there was a reason.
|
||
$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 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this persist or just set? Because there is a $customer->save();
below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I only set, we don't save.
Well, currently we also persist to customer due to a bug, but it should be set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps change method name to reflect this then.
/** | ||
* Checkout address form. | ||
*/ | ||
const AddressForm = ( { | ||
id = '', | ||
fields = defaultFields, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing the whole fields as default no longer make sense, fields
is now required.
assets/js/base/components/cart-checkout/address-form/prepare-address-fields.ts
Show resolved
Hide resolved
assets/js/data/cart/utils.ts
Outdated
); | ||
} ) | ||
.concat( removedKeys ) | ||
.concat( addedKeys ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @opr as I'm also not sure what this mean, but it helped get additional fields picked up and pushed.
* Default field properties. | ||
*/ | ||
export const defaultFields: AddressFields = | ||
getSetting< AddressFields >( 'defaultFields' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, there was a list of hardcoded fields here, that moved to CheckoutFields and it will include other fields with it.
@@ -13,7 +13,7 @@ | |||
(see https://github.com/woocommerce/woocommerce-blocks/blob/trunk/.github/release-initial-checklist.md#initial-preparation) | |||
--> | |||
<config name="minimum_supported_wp_version" value="6.3" /> | |||
<config name="testVersion" value="7.3-" /> | |||
<config name="testVersion" value="7.4-" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was needed because I was using class type hinting.
@@ -90,18 +93,24 @@ public function get_properties() { | |||
*/ | |||
public function sanitize_callback( $address, $request, $param ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We sanitize all incoming fields, include additional ones. Currently they're treated as text, in the future we would include things like select and checkboxes.
protected function get_additional_address_fields_schema() { | ||
$additional_fields_keys = $this->additional_fields_controller->get_address_fields_keys(); | ||
|
||
$fields = array_merge( $this->additional_fields_controller->get_core_fields(), $this->additional_fields_controller->get_additional_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 ( $address_fields as $key => $field ) { | ||
$schema[ $key ] = [ | ||
'description' => $field['label'], | ||
'type' => 'string', | ||
'context' => [ 'view', 'edit' ], | ||
'required' => true, | ||
]; | ||
} | ||
return $schema; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to add those fields to the schema so they're accepted and validated.
@@ -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 ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this key would hold all keys not in the address.
|
||
$fields = $this->additional_fields_controller->get_additional_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 | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also validate fields and key sure required fields are set, we return a set of errors alongside other missing fields.
* Introduce Additional Fields API for Checkout Block woocommerce/woocommerce-blocks#12073 * add changelog * Auto load the Blocks/Domain/Services/functions.php file * add changelog * revert test to what it was * Update text domain for translations * Ensure address data is added on the cart block too * fix lint problem --------- Co-authored-by: Thomas Roberts <[email protected]>
* Introduce Additional Fields API for Checkout Block woocommerce/woocommerce-blocks#12073 * revert test to what it was * Default to text, if the type supplied is not supported throw an error * Add type for options * Return null if somehow the select made it through without options * Make select fields type enum and add options to schema * Lint fixes * Update plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/address-form/address-form.tsx Co-authored-by: Niels Lange <[email protected]> * Update plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php Co-authored-by: Niels Lange <[email protected]> * Update checks to log errors and fail gracefully * Add field id to class names * Fix lint error * Fix short array use * Introduce Additional Fields API for Checkout Block woocommerce/woocommerce-blocks#12073 * Default to text, if the type supplied is not supported throw an error * Lint fixes * Introduce Additional Fields API for Checkout Block woocommerce/woocommerce-blocks#12073 * add support for registering checkboxes * remove extra error log * add styling * fix rebase conflit * fix rebase conflit 2 * fix linter errors * address review comments * add warning for checkbox * fix changes --------- Co-authored-by: Thomas Roberts <[email protected]> Co-authored-by: Thomas Roberts <[email protected]> Co-authored-by: Niels Lange <[email protected]>
This PR has been marked as If deemed still relevant, the pr can be kept active by ensuring it's up to date with the main branch and removing the stale label. |
Introduction
This PR introduces the ability to register additional fields in Checkout for developers. This is a step up from how it was done in the old Checkout, and a major improvement to how it's done currently in Checkout.
This API handles the following:
To be handled in the follow up issues
As such, this API takes off a lot of work that was being done manually and offers you seamless experience.
FAQ
- Can I change the position of my field relative to others? No, that's currently not supported but something in our roadmap.
- Can I remove/edit core fields (first name, last name...)? No, that's not part of the project, and will follow up in a future one.
- Can I add fields in the editor via UI? No, that's going to be the next project after this one, which will leverage the same APIs under the hood.
- I want to render a hidden field? That's not the goal of this project and therefore not possible, to pass values from Checkout to Store API, use
setExtensionData
.- I want to render a field only in billing, but not shipping (or vice versa): Fragmenting fields between Shipping and Billing would result in a confusing customer experience, so it's not supported. If you feel strongly that your field is only applicable to one use case but not the other, consider adding it to a different location, like Contact Information step or Additional Fields step.
- I don't want my address field to be saved to the customer session: That's not possible, as fields are one of 2, they're either relevant for the customer and order (think address, contact), or only relevant of the order (think order note, or delivery instructions). Therefore, address fields are always going to be saved to the customer.
Technical implementation
Store API
Values are passed from a Checkout to Store API in some places.
billing_address
andshipping_address
arguments for bothPOST /wc/store/cart/update-customer
andPOST /wc/store/checkout
./cart
endpoints as well as/checkout
endpoints will return saved address values inbilling_address
andshipping_address
.POST /wc/store/checkout
in aadditional_fields
key, in which each key => value is the namespace/name => value.Under the hood, fields are saved in customer metadata and order metadata. The keys names and where they're saved is considered implementation detail. Fields are saved under billing group, shipping group, or additional fields group. Both customer and orders get those groups.
Fetching the fields directly is discouraged as the fields name is not stable, helpers functions will be provided to fetch and set fields values programmatically.
Testing Instructions
Please consider any edge cases this change may have, and also other areas of the product this may impact.
Before testing
Testing feature gating
blocks.ini
) this will ensure your site is not in experimental mode.woocommerce_blocks_register_checkout_field
).blocks.ini
) this will ensure your site is in experimental mode.Testing the schema
OPTIONS /wp-json/wc/store/cart
, and thenOPTIONS /wp-json/wc/store/cart/update-customer
, and thenOPTIONS /wp-json/wc/store/checkout
schema.properties.shipping_address.properties
, you can seeplugin-namespace/vat-number
.schema.properties.billing_address.properties
, you can seeplugin-namespace/vat-number
.OPTIONS /wp-json/wc/store/cart/update-customer
andOPTIONS /wp-json/wc/store/checkout
endpoints[0].args.billing_address
, you can seeplugin-namespace/vat-number
.Test Store API.
GET /wc/store/cart
, get the nonce from it, add a product usingPOST /wc/store/cart/add-item
, get the nonce and cart token from it.POST /wc/store/cart/update-customer
a full shipping and billing address, without providing the custom field value and key, you should not get an error.3
POST /wc/store/checkout
with a full address and payment method, without the plugin vatTesting registering fields.
Testing field options
required
tofalse
.Store API
GET /wc/store/cart
, get the nonce from it, add a product usingPOST /wc/store/cart/add-item
, get the nonce and cart token from it.POST /wc/store/checkout
with a full address and payment method, without the plugin vatTesting error handling and invalid configs
id
. Reload your site and expect to see an error message describing the issue. Replaceid
.label
. Reload your site and expect to see an error message describing the issue. Replacelabel
.location
. Reload your site and expect to see an error message describing the issue. Replacelocation
.id
so that it no longer has a namespace, e.g. it will become'id' => 'vat-number'
. Reload your site and expect to see an error message describing the issue. Replace the namespace.'hidden' => true
. Reload your site and expect to see the field. Expect a PHP warning explaining the use of hidden is not supported.Testing fields
required
value totrue
in the registration snippet.Testing function availability
Testing locale customisation
my vat
(ormy-vat (Optional)
)VAT Number
(orVAT Number (Optional)
);Screenshots or screencast
WooCommerce Visibility
Required:
Checklist
Required:
[type]
label or a[skip-changelog]
label.Conditional:
[skip-changelog]
label is not present).