PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.3.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.3.0
1.6.1 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.3.0, at inc/blocks/register.php

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