|
12 | 12 |
|
13 | 13 | use Automattic\WooCommerce\Blocks\Package;
|
14 | 14 | use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
| 15 | +use Automattic\WooCommerce\StoreApi\StoreApi; |
| 16 | +use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; |
| 17 | +use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema; |
15 | 18 |
|
16 | 19 | /**
|
17 | 20 | * WC_Stripe_Express_Checkout_Element class.
|
@@ -110,6 +113,150 @@ public function init() {
|
110 | 113 | add_filter( 'woocommerce_cart_needs_shipping_address', [ $this, 'filter_cart_needs_shipping_address' ], 11, 1 );
|
111 | 114 |
|
112 | 115 | add_action( 'before_woocommerce_pay_form', [ $this, 'localize_pay_for_order_page_scripts' ] );
|
| 116 | + |
| 117 | + $this->setup_custom_checkout_data(); |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Perform necessary setup steps for supporting custom checkout fields in express checkout, |
| 122 | + * including registering space for the data in the Store API, |
| 123 | + * and hooking into the action that will let us process the data and update the order. |
| 124 | + * |
| 125 | + * @return void |
| 126 | + */ |
| 127 | + private function setup_custom_checkout_data() { |
| 128 | + // Register our express checkout data as extended data, which will hold |
| 129 | + // custom checkout fielddata if present. |
| 130 | + $extend_schema = StoreApi::container()->get( ExtendSchema::class ); |
| 131 | + $extend_schema->register_endpoint_data( |
| 132 | + [ |
| 133 | + 'endpoint' => CheckoutSchema::IDENTIFIER, |
| 134 | + 'namespace' => 'wc-stripe/express-checkout', |
| 135 | + 'schema_callback' => [ $this, 'get_custom_checkout_data_schema' ], |
| 136 | + ] |
| 137 | + ); |
| 138 | + |
| 139 | + // Update order based on extended data. |
| 140 | + add_action( |
| 141 | + 'woocommerce_store_api_checkout_update_order_from_request', |
| 142 | + [ $this, 'process_custom_checkout_data' ], |
| 143 | + 10, |
| 144 | + 2 |
| 145 | + ); |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * Allow third-party to validate and add custom checkout data to |
| 150 | + * express checkout orders for stores using classic, shortcode-based checkout. |
| 151 | + * |
| 152 | + * @param WC_Order $order The order to add custom checkout data to. |
| 153 | + * @param WP_REST_Request $request The request object. |
| 154 | + * @return void |
| 155 | + */ |
| 156 | + public function process_custom_checkout_data( $order, $request ) { |
| 157 | + $custom_checkout_data = $this->get_custom_checkout_data_from_request( $request ); |
| 158 | + if ( empty( $custom_checkout_data ) ) { |
| 159 | + return; |
| 160 | + } |
| 161 | + |
| 162 | + $errors = new WP_Error(); |
| 163 | + /** |
| 164 | + * Allow third-party plugins to validate custom checkout data for express checkout orders. |
| 165 | + * |
| 166 | + * To be used as a stand-in for the `woocommerce_after_checkout_validation` action. |
| 167 | + * |
| 168 | + * @since 9.6.0 |
| 169 | + * |
| 170 | + * @param array $custom_checkout_data The custom checkout data. |
| 171 | + * @param WP_Error $errors The WP_Error object, for adding errors when validation fails. |
| 172 | + */ |
| 173 | + do_action( 'wc_stripe_express_checkout_after_checkout_validation', $custom_checkout_data, $errors ); |
| 174 | + |
| 175 | + if ( $errors->has_errors() ) { |
| 176 | + $error_messages = implode( "\n", $errors->get_error_messages() ); |
| 177 | + throw new WC_Data_Exception( 'wc_stripe_express_checkout_invalid_data', $error_messages ); |
| 178 | + } |
| 179 | + |
| 180 | + /** |
| 181 | + * Allow third-party plugins to add custom checkout data for express checkout orders. |
| 182 | + * |
| 183 | + * To be used as a stand-in for the `woocommerce_checkout_update_order_meta` action. |
| 184 | + * |
| 185 | + * @since 9.6.0 |
| 186 | + * |
| 187 | + * @param integer $order_id The order ID. |
| 188 | + * @param array $custom_checkout_data The custom checkout data. |
| 189 | + */ |
| 190 | + do_action( 'wc_stripe_express_checkout_update_order_meta', $order->get_id(), $custom_checkout_data ); |
| 191 | + } |
| 192 | + |
| 193 | + /** |
| 194 | + * Get custom checkout data from the request object. |
| 195 | + * |
| 196 | + * To support custom fields in express checkout and classic checkout, |
| 197 | + * we pass custom data as extended data, i.e. under extensions. |
| 198 | + * |
| 199 | + * @param WP_REST_Request $request The request object. |
| 200 | + * @return array Custom checkout data. |
| 201 | + */ |
| 202 | + private function get_custom_checkout_data_from_request( $request ) { |
| 203 | + $extensions = $request->get_param( 'extensions' ); |
| 204 | + if ( empty( $extensions ) ) { |
| 205 | + return []; |
| 206 | + } |
| 207 | + |
| 208 | + $custom_checkout_data_json = $extensions['wc-stripe/express-checkout']['custom_checkout_data'] ?? ''; |
| 209 | + if ( empty( $custom_checkout_data_json ) ) { |
| 210 | + return []; |
| 211 | + } |
| 212 | + |
| 213 | + $custom_checkout_data = json_decode( $custom_checkout_data_json, true ); |
| 214 | + if ( empty( $custom_checkout_data ) || ! is_array( $custom_checkout_data ) ) { |
| 215 | + return []; |
| 216 | + } |
| 217 | + |
| 218 | + // Perform basic sanitization before passing to actions. |
| 219 | + $sanitized_custom_checkout_data = []; |
| 220 | + $custom_checkout_fields = $this->get_custom_checkout_fields(); |
| 221 | + foreach ( $custom_checkout_data as $key => $value ) { |
| 222 | + $field_type = $custom_checkout_fields[ $key ]['type'] ?? 'text'; |
| 223 | + $sanitized_key = sanitize_text_field( $key ); |
| 224 | + $sanitized_value = $this->get_sanitized_value( $value, $field_type ); |
| 225 | + $sanitized_custom_checkout_data[ $sanitized_key ] = $sanitized_value; |
| 226 | + } |
| 227 | + |
| 228 | + return $sanitized_custom_checkout_data; |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * Perform basic sanitization on custom checkout field values, based on the field type. |
| 233 | + * |
| 234 | + * @param string $value The value to sanitize. |
| 235 | + * @param string $type The type of the field. |
| 236 | + * @return string The sanitized value. |
| 237 | + */ |
| 238 | + private function get_sanitized_value( $value, $type ) { |
| 239 | + if ( 'textarea' === $type ) { |
| 240 | + return sanitize_textarea_field( $value ); |
| 241 | + } elseif ( 'email' === $type ) { |
| 242 | + return sanitize_email( $value ); |
| 243 | + } |
| 244 | + |
| 245 | + return sanitize_text_field( $value ); |
| 246 | + } |
| 247 | + /** |
| 248 | + * Get custom checkout data schema. |
| 249 | + * |
| 250 | + * @return array Custom checkout data schema. |
| 251 | + */ |
| 252 | + public function get_custom_checkout_data_schema() { |
| 253 | + return [ |
| 254 | + 'custom_checkout_data' => [ |
| 255 | + 'type' => [ 'string', 'null' ], |
| 256 | + 'context' => [], |
| 257 | + 'arg_options' => [], |
| 258 | + ], |
| 259 | + ]; |
113 | 260 | }
|
114 | 261 |
|
115 | 262 | /**
|
@@ -229,32 +376,87 @@ public function javascript_params() {
|
229 | 376 |
|
230 | 377 | /**
|
231 | 378 | * Retrieve custom checkout field IDs.
|
232 |
| - * TODO: Currently, we only support custom checkout fields for block checkout. |
233 |
| - * We need to add support for classic checkout custom fields. |
234 | 379 | *
|
235 | 380 | * @return array Custom checkout field IDs.
|
236 | 381 | */
|
237 | 382 | public function get_custom_checkout_fields() {
|
238 |
| - try { |
239 |
| - $checkout_fields = Package::container()->get( CheckoutFields::class ); |
240 |
| - if ( ! $checkout_fields instanceof CheckoutFields ) { |
| 383 | + // Block checkout page |
| 384 | + if ( has_block( 'woocommerce/checkout' ) ) { |
| 385 | + try { |
| 386 | + $checkout_fields = Package::container()->get( CheckoutFields::class ); |
| 387 | + if ( ! $checkout_fields instanceof CheckoutFields ) { |
| 388 | + return []; |
| 389 | + } |
| 390 | + |
| 391 | + $custom_checkout_fields = []; |
| 392 | + $additional_fields = $checkout_fields->get_additional_fields(); |
| 393 | + foreach ( $additional_fields as $field_key => $field ) { |
| 394 | + $location = $checkout_fields->get_field_location( $field_key ); |
| 395 | + $custom_checkout_fields[ $field_key ] = [ |
| 396 | + 'type' => $field['type'], |
| 397 | + 'location' => $location, |
| 398 | + ]; |
| 399 | + } |
| 400 | + |
| 401 | + return $custom_checkout_fields; |
| 402 | + } catch ( Exception $e ) { |
241 | 403 | return [];
|
242 | 404 | }
|
| 405 | + } |
243 | 406 |
|
244 |
| - $custom_checkout_fields = []; |
245 |
| - $additional_fields = $checkout_fields->get_additional_fields(); |
246 |
| - foreach ( $additional_fields as $field_key => $field ) { |
247 |
| - $location = $checkout_fields->get_field_location( $field_key ); |
248 |
| - $custom_checkout_fields[ $field_key ] = [ |
249 |
| - 'key' => $field_key, |
250 |
| - 'location' => $location, |
251 |
| - ]; |
| 407 | + // Classic checkout page |
| 408 | + if ( is_checkout() ) { |
| 409 | + $custom_checkout_fields = []; |
| 410 | + $standard_checkout_fields = $this->get_standard_checkout_fields(); |
| 411 | + $all_fields = WC()->checkout()->get_checkout_fields(); |
| 412 | + foreach ( $all_fields as $fieldset => $fields ) { |
| 413 | + foreach ( $fields as $field_key => $field ) { |
| 414 | + if ( in_array( $field_key, $standard_checkout_fields, true ) ) { |
| 415 | + continue; |
| 416 | + } |
| 417 | + |
| 418 | + $custom_checkout_fields[ $field_key ] = [ |
| 419 | + 'type' => $field['type'], |
| 420 | + 'location' => $fieldset, |
| 421 | + ]; |
| 422 | + } |
252 | 423 | }
|
253 | 424 |
|
254 | 425 | return $custom_checkout_fields;
|
255 |
| - } catch ( Exception $e ) { |
256 |
| - return []; |
257 | 426 | }
|
| 427 | + |
| 428 | + // Not a checkout page, e.g. product page, cart page. |
| 429 | + return []; |
| 430 | + } |
| 431 | + |
| 432 | + /** |
| 433 | + * Get standard checkout fields. |
| 434 | + * |
| 435 | + * @return array Standard checkout fields. |
| 436 | + */ |
| 437 | + private function get_standard_checkout_fields() { |
| 438 | + $default_address_fields = WC()->countries->get_default_address_fields(); |
| 439 | + $standard_billing_fields = array_map( |
| 440 | + function ( $field ) { |
| 441 | + return 'billing_' . $field; |
| 442 | + }, |
| 443 | + array_keys( $default_address_fields ) |
| 444 | + ); |
| 445 | + |
| 446 | + $standard_shipping_fields = array_map( |
| 447 | + function ( $field ) { |
| 448 | + return 'shipping_' . $field; |
| 449 | + }, |
| 450 | + array_keys( $default_address_fields ) |
| 451 | + ); |
| 452 | + |
| 453 | + $standard_checkout_fields = array_merge( |
| 454 | + $standard_billing_fields, |
| 455 | + $standard_shipping_fields, |
| 456 | + [ 'billing_phone', 'billing_email', 'order_comments' ] |
| 457 | + ); |
| 458 | + |
| 459 | + return $standard_checkout_fields; |
258 | 460 | }
|
259 | 461 |
|
260 | 462 | /**
|
|
0 commit comments