PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.2.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.2.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / blocks / register.php

register.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.2.0, at inc/blocks/register.php

504 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Blocks Register
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\Blocks;
9
10 use SureDonation\Inc\Payments\Offline\Offline_Helper;
11 use SureDonation\Inc\Payments\Payment_Helper;
12 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
13 use SureDonation\Inc\Traits\Get_Instance;
14
15 // Exit if accessed directly.
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Register class for blocks.
22 *
23 * @since 0.0.1
24 */
25 class Register {
26 use Get_Instance;
27
28 /**
29 * Constructor.
30 *
31 * @since 0.0.1
32 */
33 public function __construct() {
34 add_action( 'init', [ $this, 'register_embed_block_script' ], 5 );
35 add_action( 'init', [ $this, 'register_blocks' ] );
36 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
37 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_campaign_editor_assets' ] );
38 add_filter( 'block_categories_all', [ $this, 'register_block_category' ], 10, 2 );
39 add_filter( 'block_editor_settings_all', [ $this, 'add_campaign_iframe_styles' ], 10, 2 );
40 add_filter( 'block_editor_settings_all', [ $this, 'add_phone_iframe_styles' ], 10, 2 );
41 }
42
43 /**
44 * Register the donation form embed block editor script.
45 *
46 * Runs before register_blocks() so the handle exists when block.json is read.
47 * Not gated by post type — the embed block should work on all post types.
48 *
49 * @return void
50 * @since 1.0.0
51 */
52 public function register_embed_block_script() {
53 $asset_file = SUREDONATION_DIR . 'assets/build/blocks/donation-form/editor.asset.php';
54 $asset = file_exists( $asset_file )
55 ? require $asset_file
56 : [
57 'dependencies' => [],
58 'version' => SUREDONATION_VER,
59 ];
60
61 wp_register_script(
62 'suredonation-donation-form-editor',
63 SUREDONATION_URL . 'assets/build/blocks/donation-form/editor.js',
64 $asset['dependencies'],
65 $asset['version'],
66 true
67 );
68
69 // Data for the block editor placeholder (logo). The campaign blocks
70 // bundle defines the same global elsewhere; localizing it here keeps the
71 // logo available wherever the donation form block is inserted.
72 wp_localize_script(
73 'suredonation-donation-form-editor',
74 'suredonationCampaignBlocks',
75 $this->get_campaign_blocks_data()
76 );
77
78 wp_register_style(
79 'suredonation-donation-form-editor',
80 SUREDONATION_URL . 'assets/build/blocks/donation-form/editor.css',
81 [],
82 $asset['version']
83 );
84 }
85
86 /**
87 * Data localized for the block editor placeholders (logo).
88 *
89 * Shared by the donation form embed block and the campaign display blocks,
90 * both of which expose it on the `suredonationCampaignBlocks` JS global.
91 *
92 * `currentPostType` lets a block scope its editor registration to a single
93 * post type (the Campaign Donate Button registers only on the campaign
94 * editor). It is read from the current screen, so it is only populated for
95 * the caller that runs on `enqueue_block_editor_assets` (the campaign editor
96 * assets); the embed-block caller runs on `init`, where there is no screen,
97 * so it receives an empty string. That is harmless — the embed block only
98 * consumes `logoUrl`.
99 *
100 * @return array<string, string>
101 * @since 1.0.0
102 */
103 public function get_campaign_blocks_data() {
104 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
105
106 return [
107 'logoUrl' => esc_url_raw( SUREDONATION_URL . 'images/suredonation-logo.svg' ),
108 'currentPostType' => $screen ? (string) $screen->post_type : '',
109 ];
110 }
111
112 /**
113 * Register custom block category for SureDonation blocks.
114 *
115 * The field-block category is limited to the donation form editor; the
116 * campaign display-block category is registered everywhere else.
117 *
118 * @param array<int, array<string, mixed>> $categories Existing block categories.
119 * @param \WP_Block_Editor_Context $context Block editor context.
120 * @return array<int, array<string, mixed>> Modified block categories.
121 * @since 0.0.1
122 */
123 public function register_block_category( $categories, $context ) {
124 // Field-block category on the donation form editor.
125 if ( isset( $context->post ) && 'suredonation_form' === $context->post->post_type ) {
126 return array_merge(
127 [
128 [
129 'slug' => 'suredonation',
130 'title' => __( 'General Fields', 'suredonation' ),
131 'icon' => null,
132 ],
133 ],
134 $categories
135 );
136 }
137
138 // Campaign display-block category on every other editor — including the
139 // Site Editor and widget contexts where $context->post is unset — so the
140 // campaign blocks always group under SureDonation in the inserter. Only
141 // the donation form editor (handled above) is excluded.
142 return array_merge(
143 [
144 [
145 'slug' => 'suredonation-campaign',
146 'title' => __( 'SureDonation', 'suredonation' ),
147 'icon' => null,
148 ],
149 ],
150 $categories
151 );
152 }
153
154 /**
155 * Enqueue the campaign display blocks editor bundle.
156 *
157 * Loads on every block editor so the campaign blocks can be added to any
158 * page/post/CPT — except the donation form editor, which has its own field
159 * blocks. On a campaign post the blocks auto-bind to that campaign; elsewhere
160 * the block inspector exposes a campaign selector.
161 *
162 * @return void
163 * @since 1.0.0
164 */
165 public function enqueue_campaign_editor_assets() {
166 $screen = get_current_screen();
167
168 // Load everywhere except the donation form editor.
169 if ( ! $screen || 'suredonation_form' === $screen->post_type ) {
170 return;
171 }
172
173 $asset_file = SUREDONATION_DIR . 'assets/build/campaign-blocks.asset.php';
174 $asset = file_exists( $asset_file )
175 ? require $asset_file
176 : [
177 'dependencies' => [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-block-editor', 'wp-data', 'wp-server-side-render' ],
178 'version' => SUREDONATION_VER,
179 ];
180
181 wp_enqueue_script(
182 'suredonation-campaign-blocks',
183 SUREDONATION_URL . 'assets/build/campaign-blocks.js',
184 $asset['dependencies'],
185 $asset['version'],
186 true
187 );
188
189 wp_set_script_translations( 'suredonation-campaign-blocks', 'suredonation' );
190
191 // Data for the campaign block editor placeholder (logo).
192 wp_localize_script(
193 'suredonation-campaign-blocks',
194 'suredonationCampaignBlocks',
195 $this->get_campaign_blocks_data()
196 );
197
198 // Style the server-side-rendered block previews in the editor.
199 $style_file = SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css';
200 $style_version = file_exists( $style_file )
201 ? (string) filemtime( $style_file )
202 : SUREDONATION_VER;
203
204 wp_enqueue_style(
205 'suredonation-campaign-blocks',
206 SUREDONATION_URL . 'assets/build/blocks/campaign/style-style.css',
207 [],
208 $style_version
209 );
210 }
211
212 /**
213 * Inject the campaign block styles into the editor canvas iframe.
214 *
215 * Styles enqueued via enqueue_block_editor_assets load in the editor's outer
216 * frame only; the block canvas is iframed, so the server-side-rendered campaign
217 * block previews would otherwise render unstyled. Adding the CSS to the editor
218 * settings makes WordPress inject it inside the iframe, matching the frontend.
219 *
220 * @param array<string, mixed> $settings Block editor settings.
221 * @param \WP_Block_Editor_Context $context Block editor context.
222 * @return array<string, mixed> Modified settings.
223 * @since 1.0.0
224 */
225 public function add_campaign_iframe_styles( $settings, $context ) {
226 // Inject wherever the campaign blocks can be used (everywhere except the
227 // donation form editor), so their editor previews match the frontend.
228 if ( ! isset( $context->post ) || 'suredonation_form' === $context->post->post_type ) {
229 return $settings;
230 }
231
232 $css = $this->get_campaign_iframe_css();
233 if ( '' === $css ) {
234 return $settings;
235 }
236
237 if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
238 $settings['styles'] = [];
239 }
240
241 $settings['styles'][] = [ 'css' => $css ];
242
243 return $settings;
244 }
245
246 /**
247 * Read the built campaign stylesheet, cached per request by file mtime so
248 * the filter (which can run more than once per load) reads from disk at most
249 * once until the asset changes.
250 *
251 * @return string The stylesheet contents, or '' when unavailable.
252 * @since 1.0.0
253 */
254 private function get_campaign_iframe_css() {
255 static $cached_css = null;
256 static $cached_mtime = null;
257
258 $style_file = SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css';
259 if ( ! file_exists( $style_file ) ) {
260 return '';
261 }
262
263 $mtime = filemtime( $style_file );
264 if ( null === $cached_css || $cached_mtime !== $mtime ) {
265 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the plugin's own built stylesheet to inline into the editor iframe.
266 $css = file_get_contents( $style_file );
267 $cached_css = false === $css ? '' : $css;
268 $cached_mtime = $mtime;
269 }
270
271 return $cached_css;
272 }
273
274 /**
275 * Inject the intl-tel-input stylesheet into the editor canvas iframe.
276 *
277 * The phone block renders the real intl-tel-input control in the editor so
278 * its preview (flag + dial code) matches the front end. The library's CSS is
279 * needed inside the canvas, which is iframed, so we add it to the editor
280 * settings (the same mechanism used for the campaign block previews) rather
281 * than enqueuing it in the outer frame where the iframe can't reach it.
282 * Gated to the donation form editor, where the phone block lives.
283 *
284 * @param array<string, mixed> $settings Block editor settings.
285 * @param \WP_Block_Editor_Context $context Block editor context.
286 * @return array<string, mixed> Modified settings.
287 * @since 1.1.1
288 */
289 public function add_phone_iframe_styles( $settings, $context ) {
290 // Only the donation form editor uses the field blocks (incl. phone).
291 if ( ! isset( $context->post ) || 'suredonation_form' !== $context->post->post_type ) {
292 return $settings;
293 }
294
295 $css = $this->get_phone_iframe_css();
296 if ( '' === $css ) {
297 return $settings;
298 }
299
300 if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
301 $settings['styles'] = [];
302 }
303
304 $settings['styles'][] = [ 'css' => $css ];
305
306 return $settings;
307 }
308
309 /**
310 * Read the vendored intl-tel-input stylesheet, cached per request by file
311 * mtime so the filter (which can run more than once per load) reads from disk
312 * at most once until the asset changes.
313 *
314 * @return string The stylesheet contents, or '' when unavailable.
315 * @since 1.1.1
316 */
317 private function get_phone_iframe_css() {
318 static $cached_css = null;
319 static $cached_mtime = null;
320
321 $style_file = SUREDONATION_DIR . 'assets/css/vendor/intl/intlTelInput.min.css';
322 if ( ! file_exists( $style_file ) ) {
323 return '';
324 }
325
326 $mtime = filemtime( $style_file );
327 if ( null === $cached_css || $cached_mtime !== $mtime ) {
328 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the plugin's vendored stylesheet to inline into the editor iframe.
329 $css = file_get_contents( $style_file );
330
331 if ( false === $css ) {
332 $cached_css = '';
333 } else {
334 // The stylesheet references the flag/globe sprites with paths
335 // relative to its own location (../intl/img/…). Inlining drops
336 // that base, so rewrite them to absolute plugin URLs so the
337 // flags resolve inside the iframe.
338 $img_url = SUREDONATION_URL . 'assets/css/vendor/intl/img/';
339 $cached_css = str_replace( '../intl/img/', $img_url, $css );
340 }
341
342 $cached_mtime = $mtime;
343 }
344
345 return $cached_css;
346 }
347
348 /**
349 * Enqueue block editor assets.
350 *
351 * Only loads on the donation form editor.
352 *
353 * @return void
354 * @since 0.0.1
355 */
356 public function enqueue_editor_assets() {
357 $screen = get_current_screen();
358
359 // Only load on donation form editor.
360 if ( ! $screen || 'suredonation_form' !== $screen->post_type ) {
361 return;
362 }
363
364 // Use the asset.php content hash as the version so rebuilds bust the
365 // browser cache. Falls back to SUREDONATION_VER if the asset file
366 // is missing.
367 $blocks_asset_file = SUREDONATION_DIR . 'assets/build/blocks.asset.php';
368 $blocks_asset = file_exists( $blocks_asset_file )
369 ? require $blocks_asset_file
370 : [
371 'dependencies' => [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-block-editor', 'wp-data' ],
372 'version' => SUREDONATION_VER,
373 ];
374
375 // Enqueue the blocks script.
376 wp_enqueue_script(
377 'suredonation-blocks',
378 SUREDONATION_URL . 'assets/build/blocks.js',
379 $blocks_asset['dependencies'],
380 $blocks_asset['version'],
381 true
382 );
383
384 // Load JS translations for blocks.
385 wp_set_script_translations( 'suredonation-blocks', 'suredonation' );
386
387 // Localize script with admin data for blocks.
388 $global_currency = Payment_Helper::get_currency();
389
390 wp_localize_script(
391 'suredonation-blocks',
392 'suredonation_admin',
393 [
394 'payments' => [
395 'stripe_connected' => Stripe_Helper::is_stripe_connected(),
396 'stripe_connect_url' => Stripe_Helper::get_stripe_connect_url(),
397 'settings_url' => admin_url( 'admin.php?page=suredonation#/settings?tab=payments' ),
398 'offline_enabled' => Offline_Helper::is_offline_enabled(),
399 'gateways' => apply_filters(
400 'suredonation_editor_payment_gateways',
401 [
402 [
403 'value' => 'stripe',
404 'label' => __( 'Stripe', 'suredonation' ),
405 'supports_recurring' => true,
406 ],
407 [
408 'value' => 'offline',
409 'label' => __( 'Offline Donations', 'suredonation' ),
410 'supports_recurring' => false,
411 ],
412 ]
413 ),
414 ],
415 'fee_recovery' => Payment_Helper::get_fee_recovery_settings(),
416 'currency' => $global_currency,
417 'currencySymbol' => Payment_Helper::get_currency_symbol( $global_currency ),
418 // Resolved default validation messages so the editor can show
419 // them as placeholders on each field's Error Message control.
420 'validationMessages' => \SureDonation\Inc\Field_Validation::get_resolved_validation_messages(),
421 ]
422 );
423 }
424
425 /**
426 * Register all blocks.
427 *
428 * @return void
429 * @since 0.0.1
430 */
431 public function register_blocks() {
432 $blocks = [
433 [
434 'dir' => SUREDONATION_DIR . 'inc/blocks/**/*.php',
435 'namespace' => 'SureDonation\\Inc\\Blocks',
436 ],
437 ];
438
439 /**
440 * Filter to add and register additional blocks.
441 *
442 * @param array<int, array<string, string>> $additional_blocks Additional blocks to register.
443 */
444 $additional_blocks = apply_filters( 'suredonation_register_additional_blocks', [] );
445
446 if ( ! empty( $additional_blocks ) && is_array( $additional_blocks ) && count( $additional_blocks ) > 0 ) {
447 $blocks = [ ...$blocks, ...$additional_blocks ];
448 }
449
450 foreach ( $blocks as $block ) {
451 if ( ! is_array( $block ) || ! isset( $block['dir'] ) || ! isset( $block['namespace'] ) ) {
452 continue;
453 }
454 $block_files = glob( $block['dir'] );
455 if ( is_array( $block_files ) ) {
456 $this->register_block( $block_files, $block['namespace'], 'Block' );
457 }
458 }
459 }
460
461 /**
462 * Register blocks from directory.
463 *
464 * @param array<int, string> $blocks_dir Array of block file paths.
465 * @param string $block_namespace Block namespace.
466 * @param string $base Base class name.
467 * @return void
468 * @since 0.0.1
469 */
470 public function register_block( $blocks_dir, $block_namespace, $base ) {
471 if ( empty( $blocks_dir ) ) {
472 return;
473 }
474
475 foreach ( $blocks_dir as $filename ) {
476 // Skip base.php and register.php.
477 $basename = basename( $filename );
478 if ( 'base.php' === $basename || 'register.php' === $basename ) {
479 continue;
480 }
481
482 require_once $filename;
483
484 // Replace hyphens with underscores in directory name.
485 $classname = str_replace( '-', '_', basename( dirname( $filename ) ) );
486
487 // Convert to title case.
488 $classname = ucwords( $classname, '_' );
489
490 $full_class_name = $block_namespace . '\\' . $classname . '\\' . $base;
491
492 // Check if the class exists.
493 if ( class_exists( $full_class_name ) ) {
494 $block = new $full_class_name();
495
496 // Call register on the block object.
497 if ( method_exists( $block, 'register' ) ) {
498 $block->register();
499 }
500 }
501 }
502 }
503 }
504