PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
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
← All changes | inc/blocks/register.php +666 -10 0.0.1 → 1.6.1 View file →
@@ -6,9 +6,15 @@
6 6 */
7 7
8 8 namespace SureDonation\Inc\Blocks;
9 9
10 +use SureDonation\Inc\Assets\Register as Assets_Register;
11 +use SureDonation\Inc\Helper;
12 +use SureDonation\Inc\Payments\Offline\Offline_Helper;
13 +use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
14 +use SureDonation\Inc\Payments\Payment_Helper;
10 15 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
16 +use SureDonation\Inc\Post_Types\Donation_Form;
11 17 use SureDonation\Inc\Traits\Get_Instance;
12 18
13 19 // Exit if accessed directly.
14 20 if ( ! defined( 'ABSPATH' ) ) {
@@ -23,22 +29,256 @@
23 29 class Register {
24 30 use Get_Instance;
25 31
26 32 /**
33 + * Memoized block-inserter preview map (block key => image URL).
34 + *
35 + * Built once per request in get_field_preview_images() so every localized
36 + * copy is identical and the `suredonation_block_preview_images` filter runs
37 + * once.
38 + *
39 + * @var array<string,string>|null
40 + * @since 1.5.0
41 + */
42 + private $preview_images = null;
43 +
44 + /**
27 45 * Constructor.
28 46 *
29 47 * @since 0.0.1
30 48 */
31 49 public function __construct() {
50 + add_action( 'init', [ $this, 'register_embed_block_script' ], 5 );
32 51 add_action( 'init', [ $this, 'register_blocks' ] );
33 52 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
53 + add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_campaign_editor_assets' ] );
54 + add_action( 'enqueue_block_editor_assets', [ $this, 'localize_embed_preview_images' ] );
34 55 add_filter( 'block_categories_all', [ $this, 'register_block_category' ], 10, 2 );
56 + add_filter( 'block_editor_settings_all', [ $this, 'add_campaign_iframe_styles' ], 10, 2 );
57 + add_filter( 'block_editor_settings_all', [ $this, 'add_donation_form_iframe_styles' ], 10, 2 );
58 + add_filter( 'block_editor_settings_all', [ $this, 'add_phone_iframe_styles' ], 10, 2 );
59 + add_action( 'enqueue_block_assets', [ $this, 'enqueue_preview_field_scripts' ] );
35 60 }
36 61
37 62 /**
63 + * Load the dropdown and phone field libraries into the block editor canvas.
64 + *
65 + * The donation form embed block previews the real form through the block's PHP
66 + * render_callback (ServerSideRender), which returns markup only — a REST render
67 + * emits no wp_footer(), so nothing the render callback enqueues ever reaches the
68 + * page. Without their libraries the dropdown stays an unstyled native <select>
69 + * and the phone field renders with no country flag or dial code.
70 + *
71 + * `enqueue_block_assets` is the hook WordPress replays when it collects assets
72 + * for the iframed canvas: _wp_get_iframed_editor_assets() fires it and returns
73 + * both the printed styles AND scripts, which the canvas injects into its own
74 + * document. It is explicitly the hook for front-end assets that need to run
75 + * against editor content.
76 + *
77 + * Only the two field libraries are loaded. The payment gateways are excluded on
78 + * purpose: mounting Stripe Elements or the PayPal SDK would pull third-party
79 + * scripts into wp-admin on every editor load and open live gateway connections
80 + * for a preview the author cannot interact with. Those keep their static
81 + * placeholders (see _editor-preview.scss).
82 + *
83 + * Both initialisers are safe here — each is ready-state aware, guards against
84 + * double-initialising via a dataset flag, and exposes a re-init hook the editor
85 + * calls once ServerSideRender has injected the markup (see the block's edit
86 + * component).
87 + *
88 + * @return void
89 + * @since 1.4.0
90 + */
91 + public function enqueue_preview_field_scripts() {
92 + // Front end already enqueues these per-block from the field render; this is
93 + // the editor-only path.
94 + if ( ! is_admin() ) {
95 + return;
96 + }
97 +
98 + // The form builder mounts its own React controls for these fields.
99 + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
100 + if ( $screen && 'suredonation_form' === $screen->post_type ) {
101 + return;
102 + }
103 +
104 + // The handles are registered on wp_enqueue_scripts, which never fires in
105 + // admin. Registration is side-effect free (wp_register_* only), so reuse it
106 + // rather than duplicating the definitions.
107 + Assets_Register::get_instance()->register_frontend_assets();
108 +
109 + // Each script handle already depends on its vendor library, so enqueuing the
110 + // initialiser pulls the library in, in the right order.
111 + wp_enqueue_style( 'suredonation-tom-select' );
112 + wp_enqueue_script( 'suredonation-dropdown' );
113 + wp_enqueue_style( 'suredonation-intl-tel-input' );
114 + wp_enqueue_script( 'suredonation-phone' );
115 +
116 + // The payment bundle mounts Stripe Elements and the PayPal buttons. Both are
117 + // client-only on mount: Stripe's elements()/mount() builds an iframe, and
118 + // PayPal's createOrder does not run until the button is clicked, so nothing
119 + // here reaches the server or creates a PaymentIntent.
120 + wp_enqueue_script( 'suredonation-form-frontend' );
121 +
122 + $this->enqueue_preview_gateway_assets();
123 + }
124 +
125 + /**
126 + * Enqueue gateway assets for each donation form embedded in the current post.
127 + *
128 + * The PayPal SDK is enqueued by gateway code hooked to
129 + * `suredonation_enqueue_form_frontend_scripts`, which the render callback fires
130 + * with the form's id and content — that hook is how a gateway decides whether it
131 + * is even used by the form. A REST render throws the enqueue away, so fire it
132 + * here instead, for the forms this post actually embeds.
133 + *
134 + * Resolving the forms (rather than loading every gateway unconditionally) keeps
135 + * the gateway's own `form_has_paypal()` style gating intact, so a post with no
136 + * PayPal-enabled form does not pull the SDK into wp-admin.
137 + *
138 + * Also localises the payment settings the bundle reads, per form.
139 + *
140 + * @return void
141 + * @since 1.4.0
142 + */
143 + private function enqueue_preview_gateway_assets() {
144 + $post = get_post();
145 +
146 + if ( ! $post instanceof \WP_Post || ! has_block( 'suredonation/donation-form', $post ) ) {
147 + return;
148 + }
149 +
150 + foreach ( parse_blocks( $post->post_content ) as $block ) {
151 + if ( 'suredonation/donation-form' !== ( $block['blockName'] ?? '' ) ) {
152 + continue;
153 + }
154 +
155 + $form_id = absint( $block['attrs']['formId'] ?? 0 );
156 + $form = $form_id ? get_post( $form_id ) : null;
157 +
158 + if ( ! $form instanceof \WP_Post || Donation_Form::POST_TYPE !== $form->post_type ) {
159 + continue;
160 + }
161 +
162 + /** This action is documented in inc/blocks/donation-form/block.php */
163 + do_action( 'suredonation_enqueue_form_frontend_scripts', $form_id, $form->post_content );
164 +
165 + wp_localize_script(
166 + 'suredonation-form-frontend',
167 + 'suredonationPayment',
168 + Helper::get_form_payment_settings( $form_id )
169 + );
170 + }
171 + }
172 +
173 + /**
174 + * Register the donation form embed block editor script.
175 + *
176 + * Runs before register_blocks() so the handle exists when block.json is read.
177 + * Not gated by post type — the embed block should work on all post types.
178 + *
179 + * @return void
180 + * @since 1.0.0
181 + */
182 + public function register_embed_block_script() {
183 + $asset_file = SUREDONATION_DIR . 'assets/build/blocks/donation-form/editor.asset.php';
184 + $asset = file_exists( $asset_file )
185 + ? require $asset_file
186 + : [
187 + 'dependencies' => [],
188 + 'version' => SUREDONATION_VER,
189 + ];
190 +
191 + wp_register_script(
192 + 'suredonation-donation-form-editor',
193 + SUREDONATION_URL . 'assets/build/blocks/donation-form/editor.js',
194 + $asset['dependencies'],
195 + $asset['version'],
196 + true
197 + );
198 +
199 + // Data for the block editor placeholder (logo). The campaign blocks
200 + // bundle defines the same global elsewhere; localizing it here keeps the
201 + // logo available wherever the donation form block is inserted.
202 + wp_localize_script(
203 + 'suredonation-donation-form-editor',
204 + 'suredonationCampaignBlocks',
205 + $this->get_campaign_blocks_data()
206 + );
207 +
208 + // Note: the inserter preview map is localized on this handle later, in
209 + // localize_embed_preview_images() on enqueue_block_editor_assets, not here
210 + // on init:5 — see that method for why.
211 +
212 + // Load JS translations for the embed editor bundle so the "Field preview"
213 + // string and the block's own UI strings can be translated.
214 + wp_set_script_translations( 'suredonation-donation-form-editor', 'suredonation' );
215 +
216 + wp_register_style(
217 + 'suredonation-donation-form-editor',
218 + SUREDONATION_URL . 'assets/build/blocks/donation-form/editor.css',
219 + [],
220 + $asset['version']
221 + );
222 + }
223 +
224 + /**
225 + * Localize the inserter preview map onto the donation-form embed script.
226 + *
227 + * The embed block is insertable on any post type, so this runs on every
228 + * editor screen (no post-type guard, unlike enqueue_editor_assets()).
229 + *
230 + * It runs on `enqueue_block_editor_assets` rather than at `init` — where the
231 + * handle is registered — deliberately: get_field_preview_images() memoizes on
232 + * its first call, so the first localizer to run fixes the map for the request.
233 + * Building it here (after `init`) lets consumers of the
234 + * `suredonation_block_preview_images` filter register on the default `init`
235 + * priority — the obvious thing to do, and what SureDonation Pro does on
236 + * `plugins_loaded` — and still be captured. The handle is registered by
237 + * register_embed_block_script() on init:5, so wp_localize_script attaches here.
238 + *
239 + * @return void
240 + * @since 1.5.0
241 + */
242 + public function localize_embed_preview_images() {
243 + wp_localize_script(
244 + 'suredonation-donation-form-editor',
245 + 'suredonation_fields_preview',
246 + $this->get_field_preview_images()
247 + );
248 + }
249 +
250 + /**
251 + * Data localized for the block editor placeholders (logo).
252 + *
253 + * Shared by the donation form embed block and the campaign display blocks,
254 + * both of which expose it on the `suredonationCampaignBlocks` JS global.
255 + *
256 + * `currentPostType` lets a block scope its editor registration to a single
257 + * post type (the Campaign Donate Button registers only on the campaign
258 + * editor). It is read from the current screen, so it is only populated for
259 + * the caller that runs on `enqueue_block_editor_assets` (the campaign editor
260 + * assets); the embed-block caller runs on `init`, where there is no screen,
261 + * so it receives an empty string. That is harmless — the embed block only
262 + * consumes `logoUrl`.
263 + *
264 + * @return array<string, string>
265 + * @since 1.0.0
266 + */
267 + public function get_campaign_blocks_data() {
268 + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
269 +
270 + return [
271 + 'logoUrl' => esc_url_raw( SUREDONATION_URL . 'images/suredonation-logo.svg' ),
272 + 'currentPostType' => $screen ? (string) $screen->post_type : '',
273 + ];
274 + }
275 +
276 + /**
38 277 * Register custom block category for SureDonation blocks.
39 278 *
40 - * Only adds the category on the donation form editor.
279 + * The field-block category is limited to the donation form editor; the
280 + * campaign display-block category is registered everywhere else.
41 281 *
42 282 * @param array<int, array<string, mixed>> $categories Existing block categories.
43 283 * @param \WP_Block_Editor_Context $context Block editor context.
44 284 * @return array<int, array<string, mixed>> Modified block categories.
@@ -44,19 +284,31 @@
44 284 * @return array<int, array<string, mixed>> Modified block categories.
45 285 * @since 0.0.1
46 286 */
47 287 public function register_block_category( $categories, $context ) {
48 - // Only show category on donation form editor.
49 - if ( ! isset( $context->post ) || 'suredonation_form' !== $context->post->post_type ) {
50 - return $categories;
288 + // Field-block category on the donation form editor.
289 + if ( isset( $context->post ) && 'suredonation_form' === $context->post->post_type ) {
290 + return array_merge(
291 + [
292 + [
293 + 'slug' => 'suredonation',
294 + 'title' => __( 'General Fields', 'suredonation' ),
295 + 'icon' => null,
296 + ],
297 + ],
298 + $categories
299 + );
51 300 }
52 301
53 - // Add SureDonation category at the beginning.
302 + // Campaign display-block category on every other editor — including the
303 + // Site Editor and widget contexts where $context->post is unset — so the
304 + // campaign blocks always group under SureDonation in the inserter. Only
305 + // the donation form editor (handled above) is excluded.
54 306 return array_merge(
55 307 [
56 308 [
57 - 'slug' => 'suredonation',
58 - 'title' => __( 'General Fields', 'suredonation' ),
309 + 'slug' => 'suredonation-campaign',
310 + 'title' => __( 'SureDonation', 'suredonation' ),
59 311 'icon' => null,
60 312 ],
61 313 ],
62 314 $categories
@@ -63,8 +315,255 @@
63 315 );
64 316 }
65 317
66 318 /**
319 + * Enqueue the campaign display blocks editor bundle.
320 + *
321 + * Loads on every block editor so the campaign blocks can be added to any
322 + * page/post/CPT — except the donation form editor, which has its own field
323 + * blocks. On a campaign post the blocks auto-bind to that campaign; elsewhere
324 + * the block inspector exposes a campaign selector.
325 + *
326 + * @return void
327 + * @since 1.0.0
328 + */
329 + public function enqueue_campaign_editor_assets() {
330 + $screen = get_current_screen();
331 +
332 + // Load everywhere except the donation form editor.
333 + if ( ! $screen || 'suredonation_form' === $screen->post_type ) {
334 + return;
335 + }
336 +
337 + $asset_file = SUREDONATION_DIR . 'assets/build/campaign-blocks.asset.php';
338 + $asset = file_exists( $asset_file )
339 + ? require $asset_file
340 + : [
341 + 'dependencies' => [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-block-editor', 'wp-data', 'wp-server-side-render' ],
342 + 'version' => SUREDONATION_VER,
343 + ];
344 +
345 + wp_enqueue_script(
346 + 'suredonation-campaign-blocks',
347 + SUREDONATION_URL . 'assets/build/campaign-blocks.js',
348 + $asset['dependencies'],
349 + $asset['version'],
350 + true
351 + );
352 +
353 + wp_set_script_translations( 'suredonation-campaign-blocks', 'suredonation' );
354 +
355 + // Data for the campaign block editor placeholder (logo).
356 + wp_localize_script(
357 + 'suredonation-campaign-blocks',
358 + 'suredonationCampaignBlocks',
359 + $this->get_campaign_blocks_data()
360 + );
361 +
362 + // Block-inserter preview images (see withFieldPreview / get_field_preview_images()).
363 + wp_localize_script(
364 + 'suredonation-campaign-blocks',
365 + 'suredonation_fields_preview',
366 + $this->get_field_preview_images()
367 + );
368 +
369 + // No stylesheet is enqueued here. The campaign blocks only ever render in
370 + // the canvas, and add_campaign_iframe_styles() inlines this exact CSS into
371 + // it through the editor settings, where it carries no element id and so is
372 + // invisible to the compatibility pass. Enqueueing it on the outer frame as
373 + // well put its one `.editor-styles-wrapper` rule in the admin document,
374 + // which is all it takes for WordPress to clone the whole sheet into the
375 + // canvas and log "suredonation-campaign-blocks-css was added to the iframe
376 + // incorrectly" on every campaign editor load.
377 + }
378 +
379 + /**
380 + * Append an inline stylesheet to the block-editor iframe settings.
381 + *
382 + * Styles enqueued via enqueue_block_editor_assets load in the editor's outer
383 + * frame only; the block canvas is iframed, so server-side-rendered previews
384 + * would otherwise render unstyled. Adding CSS here makes WordPress inject it
385 + * inside the iframe, matching the frontend.
386 + *
387 + * @param array<string, mixed> $settings Block editor settings (by reference).
388 + * @param string $css Stylesheet contents to inline.
389 + * @return void
390 + * @since 1.4.0
391 + */
392 + private function append_iframe_style( &$settings, $css ) {
393 + if ( '' === $css ) {
394 + return;
395 + }
396 +
397 + if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
398 + $settings['styles'] = [];
399 + }
400 +
401 + $settings['styles'][] = [ 'css' => $css ];
402 + }
403 +
404 + /**
405 + * Read a stylesheet for iframe inlining, cached per request by file mtime so
406 + * the filter (which can run more than once per load) reads each file from disk
407 + * at most once until it changes.
408 + *
409 + * @param string $style_file Absolute path to the stylesheet.
410 + * @param array<string, string> $replacements Optional search => replace pairs
411 + * applied to the CSS, e.g. to rewrite
412 + * relative asset URLs to absolute
413 + * plugin URLs so they resolve inside
414 + * the iframe.
415 + * @return string The stylesheet contents, or '' when unavailable.
416 + * @since 1.4.0
417 + */
418 + private function read_iframe_css( $style_file, $replacements = [] ) {
419 + // Keyed by path so the aggregate + vendor stylesheets do not evict each
420 + // other's cache entry.
421 + static $cache = [];
422 +
423 + if ( ! file_exists( $style_file ) ) {
424 + return '';
425 + }
426 +
427 + $mtime = filemtime( $style_file );
428 + if ( ! isset( $cache[ $style_file ] ) || $cache[ $style_file ]['mtime'] !== $mtime ) {
429 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the plugin's own/vendored stylesheet to inline into the editor iframe.
430 + $css = file_get_contents( $style_file );
431 + $css = false === $css ? '' : $css;
432 +
433 + if ( '' !== $css && ! empty( $replacements ) ) {
434 + $css = str_replace( array_keys( $replacements ), array_values( $replacements ), $css );
435 + }
436 +
437 + $cache[ $style_file ] = [
438 + 'mtime' => $mtime,
439 + 'css' => $css,
440 + ];
441 + }
442 +
443 + return $cache[ $style_file ]['css'];
444 + }
445 +
446 + /**
447 + * Inject the campaign block styles into the editor canvas iframe.
448 + *
449 + * @param array<string, mixed> $settings Block editor settings.
450 + * @param \WP_Block_Editor_Context $context Block editor context.
451 + * @return array<string, mixed> Modified settings.
452 + * @since 1.0.0
453 + */
454 + public function add_campaign_iframe_styles( $settings, $context ) {
455 + // Inject wherever the campaign blocks can be used, which is every editor
456 + // except the donation form builder — matching the blocks' own registration
457 + // and add_donation_form_iframe_styles() below.
458 + //
459 + // The post is checked only when there is one. Requiring it excluded exactly
460 + // the contexts that have none: the widget editor never sets it
461 + // (wp-admin/widgets-form-blocks.php), and the Site Editor only sets it for a
462 + // numeric postId, so editing any template or template part
463 + // (postId=theme//slug) has none either. The campaign blocks are insertable in
464 + // both, and this stylesheet also carries the placeholder rules the donation
465 + // form embed block reuses, so bailing there left both unstyled.
466 + if ( isset( $context->post ) && 'suredonation_form' === $context->post->post_type ) {
467 + return $settings;
468 + }
469 +
470 + $this->append_iframe_style(
471 + $settings,
472 + $this->read_iframe_css( SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css' )
473 + );
474 +
475 + return $settings;
476 + }
477 +
478 + /**
479 + * Inject the donation form styles into the editor canvas iframe.
480 + *
481 + * The donation form embed block previews the real form via ServerSideRender,
482 + * and the canvas is iframed, so styles enqueued on the outer frame never reach
483 + * it. Three stylesheets are inlined:
484 + *
485 + * - the aggregate donation-form CSS, which also carries every field block's
486 + * styles and the editor-preview reconciliation (see _editor-preview.scss);
487 + * - the tom-select vendor CSS, which paints both the dropdown field's
488 + * server-rendered `.ts-wrapper` placeholder and the real control tom-select
489 + * mounts over it; and
490 + * - the intl-tel-input vendor CSS, for the `.iti` wrapper that library builds
491 + * around the phone input. Its flag sprites are referenced relative to the
492 + * stylesheet, so those paths are rewritten to absolute plugin URLs — inlining
493 + * drops the base they resolve against.
494 + *
495 + * Both libraries genuinely run in the canvas: they are enqueued on
496 + * enqueue_block_assets (see enqueue_preview_field_scripts) and re-initialised by
497 + * the block's edit component once ServerSideRender has injected the markup. The
498 + * payment gateways are not, so their placeholders stay static.
499 + *
500 + * @param array<string, mixed> $settings Block editor settings.
501 + * @param \WP_Block_Editor_Context $context Block editor context.
502 + * @return array<string, mixed> Modified settings.
503 + * @since 1.4.0
504 + */
505 + public function add_donation_form_iframe_styles( $settings, $context ) {
506 + // Inject wherever the embed block can be used, including the Site Editor and
507 + // widget contexts where $context->post is unset. Only the donation form
508 + // builder is excluded; it styles its own field blocks separately (see
509 + // add_phone_iframe_styles + form-editor).
510 + if ( isset( $context->post ) && 'suredonation_form' === $context->post->post_type ) {
511 + return $settings;
512 + }
513 +
514 + $this->append_iframe_style(
515 + $settings,
516 + $this->read_iframe_css( SUREDONATION_DIR . 'assets/build/blocks/donation-form/style-style.css' )
517 + );
518 + $this->append_iframe_style(
519 + $settings,
520 + $this->read_iframe_css( SUREDONATION_DIR . 'assets/css/vendor/tom-select.css' )
521 + );
522 + $this->append_iframe_style(
523 + $settings,
524 + $this->read_iframe_css(
525 + SUREDONATION_DIR . 'assets/css/vendor/intl/intlTelInput.min.css',
526 + [ '../intl/img/' => SUREDONATION_URL . 'assets/css/vendor/intl/img/' ]
527 + )
528 + );
529 +
530 + return $settings;
531 + }
532 +
533 + /**
534 + * Inject the intl-tel-input stylesheet into the editor canvas iframe.
535 + *
536 + * The phone block renders the real intl-tel-input control in the form builder
537 + * editor so its preview (flag + dial code) matches the front end. The canvas
538 + * is iframed, so the library CSS is added to the editor settings rather than
539 + * enqueued on the outer frame. Gated to the donation form editor, where the
540 + * phone block lives. (The relative flag sprite paths are rewritten to absolute
541 + * plugin URLs so they resolve inside the iframe.)
542 + *
543 + * @param array<string, mixed> $settings Block editor settings.
544 + * @param \WP_Block_Editor_Context $context Block editor context.
545 + * @return array<string, mixed> Modified settings.
546 + * @since 1.1.1
547 + */
548 + public function add_phone_iframe_styles( $settings, $context ) {
549 + // Only the donation form editor uses the field blocks (incl. phone).
550 + if ( ! isset( $context->post ) || 'suredonation_form' !== $context->post->post_type ) {
551 + return $settings;
552 + }
553 +
554 + $this->append_iframe_style(
555 + $settings,
556 + $this->read_iframe_css(
557 + SUREDONATION_DIR . 'assets/css/vendor/intl/intlTelInput.min.css',
558 + [ '../intl/img/' => SUREDONATION_URL . 'assets/css/vendor/intl/img/' ]
559 + )
560 + );
561 +
562 + return $settings;
563 + }
564 +
565 + /**
67 566 * Enqueue block editor assets.
68 567 *
69 568 * Only loads on the donation form editor.
70 569 *
@@ -78,28 +577,185 @@
78 577 if ( ! $screen || 'suredonation_form' !== $screen->post_type ) {
79 578 return;
80 579 }
81 580
581 + // Use the asset.php content hash as the version so rebuilds bust the
582 + // browser cache. Falls back to SUREDONATION_VER if the asset file
583 + // is missing.
584 + $blocks_asset_file = SUREDONATION_DIR . 'assets/build/blocks.asset.php';
585 + $blocks_asset = file_exists( $blocks_asset_file )
586 + ? require $blocks_asset_file
587 + : [
588 + 'dependencies' => [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-block-editor', 'wp-data' ],
589 + 'version' => SUREDONATION_VER,
590 + ];
591 +
82 592 // Enqueue the blocks script.
83 593 wp_enqueue_script(
84 594 'suredonation-blocks',
85 595 SUREDONATION_URL . 'assets/build/blocks.js',
86 - [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-block-editor', 'wp-data' ],
87 - SUREDONATION_VER,
596 + $blocks_asset['dependencies'],
597 + $blocks_asset['version'],
88 598 true
89 599 );
90 600
601 + // Load JS translations for blocks.
602 + wp_set_script_translations( 'suredonation-blocks', 'suredonation' );
603 +
91 604 // Localize script with admin data for blocks.
605 + $global_currency = Payment_Helper::get_currency();
606 +
92 607 wp_localize_script(
93 608 'suredonation-blocks',
94 609 'suredonation_admin',
95 610 [
96 - 'payments' => [
611 + 'payments' => [
97 612 'stripe_connected' => Stripe_Helper::is_stripe_connected(),
613 + 'paypal_connected' => PayPal_Helper::is_paypal_connected(),
98 614 'stripe_connect_url' => Stripe_Helper::get_stripe_connect_url(),
615 + // Base payments-settings URL; the editor's "Configure Payment
616 + // Account" CTA appends the block's selected gateway subpage.
617 + 'settings_url' => Payment_Helper::get_settings_url(),
618 + 'offline_enabled' => Offline_Helper::is_offline_enabled(),
619 + 'gateways' => apply_filters(
620 + 'suredonation_editor_payment_gateways',
621 + [
622 + [
623 + 'value' => 'stripe',
624 + 'label' => __( 'Stripe', 'suredonation' ),
625 + 'supports_recurring' => true,
626 + ],
627 + [
628 + 'value' => 'offline',
629 + 'label' => __( 'Offline Donations', 'suredonation' ),
630 + 'supports_recurring' => false,
631 + ],
632 + ]
633 + ),
99 634 ],
635 + 'fee_recovery' => Payment_Helper::get_fee_recovery_settings(),
636 + 'currency' => $global_currency,
637 + 'currencySymbol' => Payment_Helper::get_currency_symbol( $global_currency ),
638 + // Resolved default validation messages so the editor can show
639 + // them as placeholders on each field's Error Message control.
640 + 'validationMessages' => \SureDonation\Inc\Field_Validation::get_resolved_validation_messages(),
100 641 ]
101 642 );
643 +
644 + // Field-preview images shown in the block inserter's preview pane (mirrors
645 + // SureForms). Blocks whose block.json sets example.attributes.preview render
646 + // this image via the withFieldPreview HOC; unmapped blocks fall back to the
647 + // shared placeholder key on the JS side.
648 + wp_localize_script(
649 + 'suredonation-blocks',
650 + 'suredonation_fields_preview',
651 + $this->get_field_preview_images()
652 + );
653 + }
654 +
655 + /**
656 + * Block-inserter preview images, keyed by block name (slug, hyphens as
657 + * underscores) to mirror the JS lookup in withFieldPreview.
658 + *
659 + * Shared by every editor bundle (field blocks, campaign blocks, donation-form
660 + * embed) so the same map is localized wherever a SureDonation block can be
661 + * inserted. Field-type blocks reuse the SureForms field-preview art; campaign
662 + * and donation-form display blocks use SureDonation's own mockups; anything not
663 + * listed falls back to the shared placeholder on the JS side.
664 + *
665 + * Pro-only field blocks (date/time picker) register their own art through the
666 + * `suredonation_block_preview_images` filter — Pro owns those assets, so they
667 + * are not listed here.
668 + *
669 + * Memoized so the map is built once per request: every localized copy is then
670 + * identical regardless of which editor hook fires first, and the filter runs
671 + * exactly once.
672 + *
673 + * @return array<string,string> Map of block key => image URL.
674 + * @since 1.5.0
675 + */
676 + public function get_field_preview_images() {
677 + if ( null !== $this->preview_images ) {
678 + return $this->preview_images;
679 + }
680 +
681 + $base = SUREDONATION_URL . 'images/field-previews/';
682 +
683 + /**
684 + * Filters the block-inserter preview image map.
685 + *
686 + * Keys are block slugs with hyphens replaced by underscores
687 + * (`suredonation/date-picker` => `date_picker`) to match the JS lookup in
688 + * withFieldPreview; values are absolute image URLs.
689 + *
690 + * Register no later than `init` — SureDonation Pro adds its date/time
691 + * picker art on `plugins_loaded`. The map is first built on
692 + * `enqueue_block_editor_assets` and then memoized, so a filter added after
693 + * that first build is silently ignored.
694 + *
695 + * @since 1.5.0
696 + *
697 + * @param array<string,string> $images Map of block key => image URL.
698 + */
699 + $images = apply_filters(
700 + 'suredonation_block_preview_images',
701 + [
702 + // Field blocks.
703 + 'input' => $base . 'input.svg',
704 + 'email' => $base . 'email.svg',
705 + 'number' => $base . 'number.svg',
706 + 'checkbox' => $base . 'checkbox.svg',
707 + // A multi-line message field — the textarea art, not the text input's.
708 + 'donor_comment' => $base . 'donor-comment.svg',
709 + // Anonymous-donation and cover-fees both render a single checkbox.
710 + 'anonymous_donation' => $base . 'checkbox.svg',
711 + 'cover_fees' => $base . 'checkbox.svg',
712 + 'dropdown' => $base . 'dropdown.svg',
713 + // Donation-amount is a grid of selectable amounts (radio options).
714 + 'donation_amount' => $base . 'multi-choice.svg',
715 + 'address' => $base . 'address.svg',
716 + 'phone' => $base . 'phone.svg',
717 + 'url' => $base . 'url.svg',
718 + 'heading' => $base . 'heading.svg',
719 + 'html' => $base . 'html.svg',
720 + 'image' => $base . 'image.svg',
721 + 'payment' => $base . 'payment.svg',
722 + 'donate_button' => $base . 'button.svg',
723 + // Campaign + donation-form display blocks.
724 + 'donation_form' => $base . 'donation-form.svg',
725 + 'campaign_goal' => $base . 'campaign-goal.svg',
726 + 'campaign_stats' => $base . 'campaign-stats.svg',
727 + 'campaign_donations' => $base . 'campaign-donations.svg',
728 + 'campaign_donors' => $base . 'campaign-donors.svg',
729 + 'campaign_donor_comments' => $base . 'campaign-donor-comments.svg',
730 + 'campaign_social_sharing' => $base . 'campaign-social-sharing.svg',
731 + 'campaign_donate_button' => $base . 'button.svg',
732 + // Fallback for any block without its own image.
733 + 'placeholder' => $base . 'placeholder.svg',
734 + ]
735 + );
736 +
737 + // Coerce defensively: a filter returning a non-array or non-string values
738 + // must not poison the map (mirrors the JS-side guard).
739 + if ( ! is_array( $images ) ) {
740 + $this->preview_images = [];
741 + return $this->preview_images;
742 + }
743 +
744 + $result = [];
745 + foreach ( $images as $key => $value ) {
746 + if ( is_string( $key ) && is_string( $value ) ) {
747 + // esc_url_raw for consistency with get_campaign_blocks_data(); the
748 + // value is consumed by JS as an <img src>, not printed as HTML. The
749 + // plugin version busts the browser cache when redesigned art ships
750 + // in a release (filemtime is avoided: filter-supplied Pro URLs do
751 + // not resolve to a local path).
752 + $result[ $key ] = esc_url_raw( add_query_arg( 'ver', SUREDONATION_VER, $value ) );
753 + }
754 + }
755 +
756 + $this->preview_images = $result;
757 + return $this->preview_images;
102 758 }
103 759
104 760 /**
105 761 * Register all blocks.