should_hide_fields() ) { return $fields; } $fields['billing'] = array(); $fields['shipping'] = array(); $fields['order'] = array(); return $fields; } /** * Supply the order data the stripped fields would have collected. * * @param array $data Posted checkout data. * * @return array */ public function inject_customer_data( $data ) { $customer = $this->booking_customer(); if ( ! $customer || ! $this->should_hide_fields() ) { return $data; } foreach ( array( 'first_name', 'last_name', 'email', 'phone' ) as $key ) { if ( ! empty( $customer[ $key ] ) ) { $data[ 'billing_' . $key ] = $customer[ $key ]; } } return $data; } /** * Prefill a visible checkout field from the booking. * * @param mixed $value Current value. * @param string $input Field name. * * @return mixed */ public function prefill_checkout_field( $value, $input ) { if ( ! empty( $value ) ) { return $value; } $customer = $this->booking_customer(); if ( ! $customer ) { return $value; } $map = array( 'billing_first_name' => 'first_name', 'billing_last_name' => 'last_name', 'billing_email' => 'email', 'billing_phone' => 'phone', ); return isset( $map[ $input ], $customer[ $map[ $input ] ] ) ? $customer[ $map[ $input ] ] : $value; } /** * Hide and un-require the address fields on block checkouts. * * These locale filters are the one lever both checkouts honour. Email and * name are not address fields and stay visible on the block. * * @param array $fields Default address fields. * * @return array */ public function relax_address_fields( $fields ) { if ( ! $this->should_hide_fields() || ! $this->cart_has_booking() ) { return $fields; } foreach ( $this->address_fields_to_hide() as $key ) { if ( isset( $fields[ $key ] ) ) { $fields[ $key ]['required'] = false; $fields[ $key ]['hidden'] = true; } } return $fields; } /** * Hide and un-require the address fields on the checkout block. * * The block builds its address form from the country locale, not from * woocommerce_default_address_fields, so relax_address_fields never reaches * it. Marking the fields hidden and optional here is what actually drops * them from the block, and un-requiring them keeps the order from failing * validation once they are gone. Applied to every country's locale (plus the * shared default) so it holds whichever country the customer picks. * * @param array $locale Country locale, keyed by country code. * * @return array */ public function relax_block_address_fields( $locale ) { if ( ! $this->should_hide_fields() ) { return $locale; } foreach ( $locale as $country => $fields ) { if ( ! is_array( $fields ) ) { continue; } foreach ( $this->block_fields_to_hide() as $key ) { $locale[ $country ][ $key ]['required'] = false; $locale[ $country ][ $key ]['hidden'] = true; } } return $locale; } /** * Address field keys stripped when billing fields are turned off. * * @return array */ private function address_fields_to_hide(): array { return array( 'company', 'address_1', 'address_2', 'city', 'state', 'postcode' ); } /** * Every field the block hides in its billing group. * * Wider than the classic set on purpose: the block honours the locale for * the name and phone too, so those go as well and the booking's own values * (supplied by prefill_customer_object) keep the hidden, now-optional name * from failing validation. Two fields are deliberately left visible: email, * which lives in its own contact section and the block will not let go of, * and country, which the block always renders and, left blank, is the * selection that makes it load the payment methods in the first place. * * @return array */ private function block_fields_to_hide(): array { return array_merge( $this->address_fields_to_hide(), array( 'first_name', 'last_name', 'phone' ) ); } /** * Seed the WooCommerce customer with the booking's contact details. * * The block checkout renders from the customer object rather than from * checkout field values, so this is what prefills it there. * * @return void */ public function prefill_customer_object() { $customer = $this->booking_customer(); if ( ! $customer || ! function_exists( 'WC' ) || ! WC()->customer ) { return; } if ( ! WC()->customer->get_billing_email() && ! empty( $customer['email'] ) ) { WC()->customer->set_billing_email( $customer['email'] ); } if ( ! WC()->customer->get_billing_first_name() && ! empty( $customer['first_name'] ) ) { WC()->customer->set_billing_first_name( $customer['first_name'] ); } if ( ! WC()->customer->get_billing_last_name() && ! empty( $customer['last_name'] ) ) { WC()->customer->set_billing_last_name( $customer['last_name'] ); } if ( ! WC()->customer->get_billing_phone() && ! empty( $customer['phone'] ) ) { WC()->customer->set_billing_phone( $customer['phone'] ); } } /** * Send the customer back to the booking page when the setting says to. * * The order id and its view key travel in the URL rather than in session * storage, which Safari and in-app browsers can drop across an off-site * gateway round trip. * * @param string $url Default return URL. * @param \WC_Order $order Order just paid. * * @return string */ public function return_url( $url, $order = null ) { if ( ! $order instanceof \WC_Order ) { return $url; } if ( 'booktics' !== booktics_get_option( 'wc_thankyou_page', 'woocommerce' ) ) { return $url; } $booking = $this->booking_on_order( $order ); if ( ! $booking || empty( $booking['return_url'] ) ) { return $url; } return add_query_arg( array( 'order_id' => (int) $booking['order_id'], 'order_key' => Order_Key::make( (int) $booking['order_id'], (string) ( $booking['order_no'] ?? '' ) ), 'booktics_payment' => 'woocommerce', ), $booking['return_url'] ); } /** * Show the booking on WooCommerce's own order-received page. * * @param int $order_id Order id. * * @return void */ public function thankyou_details( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order instanceof \WC_Order ) { return; } $booking = $this->booking_on_order( $order ); if ( ! $booking ) { return; } echo '

' . esc_html__( 'Your booking', 'booktics' ) . '

    '; foreach ( $this->booking_summary( $booking ) as $label => $value ) { printf( '
  • %s: %s
  • ', esc_html( $label ), esc_html( $value ) ); } echo '
'; } /** * Readable summary rows for the thank-you page. * * @param array $booking Booking payload from the line item. * * @return array */ private function booking_summary( array $booking ): array { $rows = array(); if ( ! empty( $booking['service'] ) ) { $rows[ __( 'Service', 'booktics' ) ] = $booking['service']; } if ( ! empty( $booking['date'] ) ) { $timestamp = strtotime( $booking['date'] ); $rows[ __( 'Date', 'booktics' ) ] = $timestamp ? wp_date( get_option( 'date_format' ), $timestamp ) : $booking['date']; } if ( ! empty( $booking['start_time'] ) ) { $rows[ __( 'Time', 'booktics' ) ] = empty( $booking['end_time'] ) ? $booking['start_time'] : $booking['start_time'] . ' - ' . $booking['end_time']; } if ( ! empty( $booking['team_member'] ) ) { $rows[ __( 'Team member', 'booktics' ) ] = $booking['team_member']; } return $rows; } /** * Whether the checkout fields should be stripped for this session. * * @return bool */ private function should_hide_fields(): bool { if ( booktics_get_option( 'wc_show_billing_fields' ) ) { return false; } return $this->cart_has_booking(); } /** * Is a booking in the cart right now? * * Guards every field change so a normal shop checkout is never touched. * * @return bool */ private function cart_has_booking(): bool { return null !== $this->booking_in_cart(); } /** * Contact details of the booking currently in the cart. * * @return array|null */ private function booking_customer() { $booking = $this->booking_in_cart(); return $booking && ! empty( $booking['customer'] ) ? $booking['customer'] : null; } /** * Booking payload sitting in the cart, if any. * * @return array|null */ private function booking_in_cart() { if ( ! function_exists( 'WC' ) || ! WC()->cart instanceof \WC_Cart ) { return null; } foreach ( WC()->cart->get_cart() as $cart_item ) { if ( ! empty( $cart_item[ Cart_Handoff::CART_KEY ] ) ) { return $cart_item[ Cart_Handoff::CART_KEY ]; } } return null; } /** * Rebuild the booking payload from an order's line items. * * Used after checkout, when the cart is already gone. * * @param \WC_Order $order Order. * * @return array|null */ private function booking_on_order( $order ) { foreach ( $order->get_items() as $item ) { $booktics_order_id = (int) $item->get_meta( '_booktics_order_id' ); if ( ! $booktics_order_id ) { continue; } return array( 'order_id' => $booktics_order_id, 'order_no' => (string) $order->get_meta( '_booktics_order_no' ), 'service' => $item->get_name(), 'date' => (string) $item->get_meta( __( 'Date', 'booktics' ) ), 'start_time' => (string) $item->get_meta( __( 'Time', 'booktics' ) ), 'end_time' => '', 'team_member' => (string) $item->get_meta( __( 'Team member', 'booktics' ) ), 'return_url' => (string) $order->get_meta( '_booktics_return_url' ), ); } return null; } }