Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Bitcoin_Order_Confirmation_Block
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 7
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register_block
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
12
 render_block
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 get_order_details_formatted_array
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_order
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 detect_order_id
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
56
 add_order_id_context
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * WordPress block for Bitcoin Gateway order container.
4 *
5 * A container block that provides order context to inner blocks on WooCommerce Thank You pages.
6 *
7 * @package brianhenryie/bh-wp-bitcoin-gateway
8 */
9
10namespace BrianHenryIE\WP_Bitcoin_Gateway\Integrations\WooCommerce\Blocks;
11
12use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Exceptions\BH_WP_Bitcoin_Gateway_Exception;
13use BrianHenryIE\WP_Bitcoin_Gateway\Integrations\WooCommerce\API_WooCommerce_Interface;
14use BrianHenryIE\WP_Bitcoin_Gateway\Integrations\WooCommerce\Details_Formatter;
15use BrianHenryIE\WP_Bitcoin_Gateway\Settings_Interface;
16use WC_Order;
17use WP_Block;
18use WP_Block_Type_Registry;
19
20/**
21 * @phpstan-type ParsedBlockWithoutSubblock array{blockName:string|null, attrs:array<mixed>, innerBlocks:null, innerHTML:string, innerContent:array<int,string|null>}
22 * @phpstan-type ParsedBlock array{blockName:string|null, attrs:array<mixed>, innerBlocks:array<ParsedBlockWithoutSubblock>, innerHTML:string, innerContent:array<int,string|null>}
23 */
24class Bitcoin_Order_Confirmation_Block {
25
26    /**
27     * Constructor.
28     *
29     * @param Settings_Interface        $settings Plugin settings, plugin url required for serving script.
30     * @param API_WooCommerce_Interface $api Used to get the formatted order details.
31     */
32    public function __construct(
33        protected Settings_Interface $settings,
34        protected API_WooCommerce_Interface $api,
35    ) {
36    }
37
38    /**
39     *
40     * Hooking on `init` so even if WooCommerce is disabled, the blocks are still available for design.
41     *
42     * @hooked init
43     */
44    public function register_block(): void {
45
46        /** @var array{dependencies:array<string>, version:string} $webpack_asset */
47        $webpack_asset = include $this->settings->get_plugin_dir() . 'assets/js/frontend/woocommerce/blocks/order-confirmation/bitcoin-order-confirmation-group/bitcoin-order-confirmation-group.min.asset.php';
48
49        wp_register_script(
50            'bh-wp-bitcoin-gateway-bitcoin-order-block',
51            $this->settings->get_plugin_url() . 'assets/js/frontend/woocommerce/blocks/order-confirmation/bitcoin-order-confirmation-group/bitcoin-order-confirmation-group.min.js',
52            $webpack_asset['dependencies'],
53            $webpack_asset['version'],
54            array( 'in_footer' => true )
55        );
56
57        $provides_context = array(
58            'bh-wp-bitcoin-gateway/orderId' => 'orderId',
59        );
60        foreach ( $this->get_order_details_formatted_array() as $key => $value ) {
61            if ( is_string( $value ) ) {
62                $provides_context[ "bh-wp-bitcoin-gateway/$key" ] = $value;
63            }
64        }
65
66        register_block_type(
67            // TODO: rename to be explicitly a block for WooCommerce.
68            'bh-wp-bitcoin-gateway/bitcoin-order',
69            array(
70                // TODO: Should this be `editor_script_handles` as an array?
71                'editor_script'    => 'bh-wp-bitcoin-gateway-bitcoin-order-block',
72                'attributes'       => array(
73                    'orderId' => array(
74                        'type'    => 'number',
75                        'default' => 0,
76                    ),
77                ),
78                'provides_context' => $provides_context,
79                'render_callback'  => $this->render_block( ... ),
80            )
81        );
82
83        // TODO: move out of here for legibility.
84        add_filter(
85            'render_block_context',
86            $this->add_order_id_context( ... ),
87            10,
88            3
89        );
90    }
91
92    /**
93     * Render callback for the bitcoin-order block.
94     *
95     * @param array    $attributes Block attributes.
96     * @param string   $content    Block content.
97     * @param WP_Block $block      Block instance.
98     * @return string Rendered block content.
99     */
100    public function render_block( array $attributes, string $content, $block ): string {
101        $order_id = $attributes['orderId'] ?? 0;
102
103        if ( 0 === $order_id ) {
104            $order_id = $this->detect_order_id();
105        }
106
107        // Update the block context with the detected order ID.
108        if ( $order_id > 0 ) {
109            // TODO: This doesn't seem to do anything for PHP rendered blocks... does it help for JS rendered?
110            $block->context['bh-wp-bitcoin-gateway/orderId'] = $order_id;
111        }
112
113        $order_details_formatted = $this->get_order_details_formatted_array();
114        foreach ( $order_details_formatted as $key => $value ) {
115            $block->context[ "bh-wp-bitcoin-gateway/$key" ] = $value;
116        }
117
118        $wrapper_attributes = array(
119            'class' => 'bh-wp-bitcoin-gateway-bitcoin-order-container',
120        );
121        /**
122         * TODO: we know these are strings, link to where they are set.
123         *
124         * @var string $key
125         * @var string $value
126         */
127        foreach ( $block->context as $key => $value ) {
128            $sanitized_key = str_replace( array( ':', '/' ), '-', $key );
129            $wrapper_attributes[ 'data-context-' . $sanitized_key ] = esc_attr( (string) $value );
130        }
131        // Return the inner blocks content wrapped in our container.
132        $wrapper_attributes_string = get_block_wrapper_attributes( $wrapper_attributes );
133
134        /**
135         * TODO: I thought these would re-render the inner blocks, which were rendered before this block
136         * determined the order id it was wrapping them with.
137         *
138         * @see WP_Block::refresh_parsed_block_dependents()
139         * @see WP_Block::refresh_context_dependents()
140         */
141
142        return sprintf(
143            '<div %1$s><div class="wp-block-group"><div class="wp-block-group__inner-container">%2$s</div></div></div>',
144            $wrapper_attributes_string,
145            $content
146        );
147    }
148
149    /**
150     * Get formatted order details for display.
151     *
152     * @return array<string,mixed> The formatted order details or null if no order found.
153     */
154    protected function get_order_details_formatted_array(): array {
155        $order = $this->get_order();
156
157        if ( is_null( $order ) ) {
158            return array();
159        }
160
161        $order_details_formatted_array = $this->api->get_formatted_order_details( $order );
162        return Details_Formatter::camel_case_keys( $order_details_formatted_array );
163    }
164
165    /**
166     * Get the WooCommerce order for the current request.
167     *
168     * @return WC_Order|null The WooCommerce order or null if not found.
169     */
170    protected function get_order(): ?WC_Order {
171        if ( ! function_exists( 'wc_get_order' ) ) {
172            return null; // TODO: Add breakpoint and make sure this isn't executing unless it is needed.
173        }
174        $wc_order = wc_get_order( $this->detect_order_id() );
175        return $wc_order instanceof WC_Order ? $wc_order : null;
176    }
177
178    /**
179     * Detect the current order ID from various WooCommerce sources.
180     *
181     * @return int Order ID or 0 if not found.
182     */
183    protected function detect_order_id(): int {
184
185        if ( isset( $GLOBALS['order-received'] ) && is_numeric( $GLOBALS['order-received'] ) ) {
186            return absint( $GLOBALS['order-received'] );
187        }
188
189        // Check the key in the URL. The `key` is the ~nonce.
190        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
191        if ( function_exists( 'wc_get_order_id_by_order_key' ) && isset( $_GET['key'] ) && is_numeric( $_GET['key'] ) ) {
192            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
193            $order_id = wc_get_order_id_by_order_key( (string) absint( $_GET['key'] ) );
194            if ( $order_id > 0 ) {
195                return $order_id;
196            }
197        }
198
199        return 0;
200    }
201
202    /**
203     * Filter block context to add the order id.
204     *
205     * @hooked render_block_context
206     * @see render_block()
207     *
208     * `$parsed_block['innerBlocks'][]` is the same array shape as the array itself.
209     * TODO: Where is postID and postType set on the context?
210     *
211     * @param array{postId:int,postType:string} $context The current block context containing post ID and type from WordPress.
212     * @param array&ParsedBlock                 $parsed_block The parsed block array structure containing block name, attributes, and inner blocks from the WordPress block parser.
213     * @param ?WP_Block                         $parent_block The parent block instance if this block is nested, or null if it's a top-level block.
214     *
215     * @return array{postId:int,postType:string}|array{postId:int,'postType':string, "bh-wp-bitcoin-gateway/orderId":int}
216     */
217    public function add_order_id_context( array $context, array $parsed_block, ?WP_Block $parent_block ): array {
218
219        $block_name = $parsed_block['blockName'];
220
221        // No need to check plain HTML or ourself.
222        if ( is_null( $block_name ) || 'bh-wp-bitcoin-gateway/bitcoin-order' === $block_name ) {
223            return $context;
224        }
225
226        // Check if the block uses our context.
227        $block_uses_context = WP_Block_Type_Registry::get_instance()->get_registered( $block_name )?->get_uses_context() ?? array();
228
229        if ( ! in_array( 'bh-wp-bitcoin-gateway/orderId', $block_uses_context, true ) ) {
230            return $context;
231        }
232
233        $context['bh-wp-bitcoin-gateway/orderId'] = $this->detect_order_id();
234
235        return $context;
236    }
237}