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 +428 -93 1.2.0 → 1.6.1 View file →
@@ -6,11 +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;
10 12 use SureDonation\Inc\Payments\Offline\Offline_Helper;
13 +use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
11 14 use SureDonation\Inc\Payments\Payment_Helper;
12 15 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
16 +use SureDonation\Inc\Post_Types\Donation_Form;
13 17 use SureDonation\Inc\Traits\Get_Instance;
14 18
15 19 // Exit if accessed directly.
16 20 if ( ! defined( 'ABSPATH' ) ) {
@@ -25,8 +29,20 @@
25 29 class Register {
26 30 use Get_Instance;
27 31
28 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 + /**
29 45 * Constructor.
30 46 *
31 47 * @since 0.0.1
32 48 */
@@ -34,14 +50,128 @@
34 50 add_action( 'init', [ $this, 'register_embed_block_script' ], 5 );
35 51 add_action( 'init', [ $this, 'register_blocks' ] );
36 52 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
37 53 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_campaign_editor_assets' ] );
54 + add_action( 'enqueue_block_editor_assets', [ $this, 'localize_embed_preview_images' ] );
38 55 add_filter( 'block_categories_all', [ $this, 'register_block_category' ], 10, 2 );
39 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 );
40 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' ] );
41 60 }
42 61
43 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 + /**
44 174 * Register the donation form embed block editor script.
45 175 *
46 176 * Runs before register_blocks() so the handle exists when block.json is read.
47 177 * Not gated by post type — the embed block should work on all post types.
@@ -74,8 +204,16 @@
74 204 'suredonationCampaignBlocks',
75 205 $this->get_campaign_blocks_data()
76 206 );
77 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 +
78 216 wp_register_style(
79 217 'suredonation-donation-form-editor',
80 218 SUREDONATION_URL . 'assets/build/blocks/donation-form/editor.css',
81 219 [],
@@ -83,8 +221,34 @@
83 221 );
84 222 }
85 223
86 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 + /**
87 251 * Data localized for the block editor placeholders (logo).
88 252 *
89 253 * Shared by the donation form embed block and the campaign display blocks,
90 254 * both of which expose it on the `suredonationCampaignBlocks` JS global.
@@ -194,45 +358,41 @@
194 358 'suredonationCampaignBlocks',
195 359 $this->get_campaign_blocks_data()
196 360 );
197 361
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(
362 + // Block-inserter preview images (see withFieldPreview / get_field_preview_images()).
363 + wp_localize_script(
205 364 'suredonation-campaign-blocks',
206 - SUREDONATION_URL . 'assets/build/blocks/campaign/style-style.css',
207 - [],
208 - $style_version
365 + 'suredonation_fields_preview',
366 + $this->get_field_preview_images()
209 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.
210 377 }
211 378
212 379 /**
213 - * Inject the campaign block styles into the editor canvas iframe.
380 + * Append an inline stylesheet to the block-editor iframe settings.
214 381 *
215 382 * 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.
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.
219 386 *
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
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
224 391 */
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();
392 + private function append_iframe_style( &$settings, $css ) {
233 393 if ( '' === $css ) {
234 - return $settings;
394 + return;
235 395 }
236 396
237 397 if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
238 398 $settings['styles'] = [];
@@ -238,112 +398,169 @@
238 398 $settings['styles'] = [];
239 399 }
240 400
241 401 $settings['styles'][] = [ 'css' => $css ];
242 -
243 - return $settings;
244 402 }
245 403
246 404 /**
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.
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.
250 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.
251 415 * @return string The stylesheet contents, or '' when unavailable.
252 - * @since 1.0.0
416 + * @since 1.4.0
253 417 */
254 - private function get_campaign_iframe_css() {
255 - static $cached_css = null;
256 - static $cached_mtime = null;
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 = [];
257 422
258 - $style_file = SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css';
259 423 if ( ! file_exists( $style_file ) ) {
260 424 return '';
261 425 }
262 426
263 427 $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;
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 + ];
269 441 }
270 442
271 - return $cached_css;
443 + return $cache[ $style_file ]['css'];
272 444 }
273 445
274 446 /**
275 - * Inject the intl-tel-input stylesheet into the editor canvas iframe.
447 + * Inject the campaign block styles into the editor canvas iframe.
276 448 *
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 449 * @param array<string, mixed> $settings Block editor settings.
285 450 * @param \WP_Block_Editor_Context $context Block editor context.
286 451 * @return array<string, mixed> Modified settings.
287 - * @since 1.1.1
452 + * @since 1.0.0
288 453 */
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 ) {
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 ) {
292 467 return $settings;
293 468 }
294 469
295 - $css = $this->get_phone_iframe_css();
296 - if ( '' === $css ) {
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 ) {
297 511 return $settings;
298 512 }
299 513
300 - if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
301 - $settings['styles'] = [];
302 - }
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 + );
303 529
304 - $settings['styles'][] = [ 'css' => $css ];
305 -
306 530 return $settings;
307 531 }
308 532
309 533 /**
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.
534 + * Inject the intl-tel-input stylesheet into the editor canvas iframe.
313 535 *
314 - * @return string The stylesheet contents, or '' when unavailable.
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.
315 546 * @since 1.1.1
316 547 */
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 '';
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;
324 552 }
325 553
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 );
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 + );
330 561
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;
562 + return $settings;
346 563 }
347 564
348 565 /**
349 566 * Enqueue block editor assets.
@@ -392,10 +609,13 @@
392 609 'suredonation_admin',
393 610 [
394 611 'payments' => [
395 612 'stripe_connected' => Stripe_Helper::is_stripe_connected(),
613 + 'paypal_connected' => PayPal_Helper::is_paypal_connected(),
396 614 'stripe_connect_url' => Stripe_Helper::get_stripe_connect_url(),
397 - 'settings_url' => admin_url( 'admin.php?page=suredonation#/settings?tab=payments' ),
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(),
398 618 'offline_enabled' => Offline_Helper::is_offline_enabled(),
399 619 'gateways' => apply_filters(
400 620 'suredonation_editor_payment_gateways',
401 621 [
@@ -419,8 +639,123 @@
419 639 // them as placeholders on each field's Error Message control.
420 640 'validationMessages' => \SureDonation\Inc\Field_Validation::get_resolved_validation_messages(),
421 641 ]
422 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;
423 758 }
424 759
425 760 /**
426 761 * Register all blocks.