| 1 |
<?php |
| 2 |
/** |
| 3 |
* Donations Block. |
| 4 |
* |
| 5 |
* @since 8.x |
| 6 |
* |
| 7 |
* @package automattic/jetpack |
| 8 |
*/ |
| 9 |
|
| 10 |
namespace Automattic\Jetpack\Extensions\Donations; |
| 11 |
|
| 12 |
use Automattic\Jetpack\Blocks; |
| 13 |
use Automattic\Jetpack\Status\Host; |
| 14 |
use Automattic\Jetpack\Status\Request; |
| 15 |
use Automattic\Jetpack\Tracking; |
| 16 |
use Jetpack; |
| 17 |
use Jetpack_Gutenberg; |
| 18 |
use WP_Post; |
| 19 |
|
| 20 |
if ( ! defined( 'ABSPATH' ) ) { |
| 21 |
exit( 0 ); |
| 22 |
} |
| 23 |
|
| 24 |
/** |
| 25 |
* Registers the block for use in Gutenberg |
| 26 |
* This is done via an action so that we can disable |
| 27 |
* registration if we need to. |
| 28 |
*/ |
| 29 |
function register_block() { |
| 30 |
|
| 31 |
require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php'; |
| 32 |
if ( \Jetpack_Memberships::should_enable_monetize_blocks_in_editor() ) { |
| 33 |
Blocks::jetpack_register_block( |
| 34 |
__DIR__, |
| 35 |
array( |
| 36 |
'render_callback' => __NAMESPACE__ . '\render_block', |
| 37 |
'render_email_callback' => __NAMESPACE__ . '\render_email', |
| 38 |
'plan_check' => true, |
| 39 |
) |
| 40 |
); |
| 41 |
} |
| 42 |
// Add a meta field to the user to track if the donation warning has been dismissed. |
| 43 |
\register_meta( |
| 44 |
'user', |
| 45 |
'jetpack_donation_warning_dismissed', |
| 46 |
array( |
| 47 |
'type' => 'boolean', |
| 48 |
'single' => true, |
| 49 |
'show_in_rest' => true, |
| 50 |
'default' => false, |
| 51 |
) |
| 52 |
); |
| 53 |
} |
| 54 |
add_action( 'init', __NAMESPACE__ . '\register_block' ); |
| 55 |
|
| 56 |
/** |
| 57 |
* Donations block dynamic rendering. |
| 58 |
* |
| 59 |
* @param array $attr Array containing the Donations block attributes. |
| 60 |
* @param string $content String containing the Donations block content. |
| 61 |
* |
| 62 |
* @return string |
| 63 |
*/ |
| 64 |
function render_block( $attr, $content ) { |
| 65 |
// Keep content as-is if rendered in other contexts than frontend (i.e. feed, emails, API, etc.). |
| 66 |
if ( ! Request::is_frontend() ) { |
| 67 |
$parsed = parse_blocks( $content ); |
| 68 |
if ( ! empty( $parsed[0] ) ) { |
| 69 |
// Inject the link of the current post from the server side as the fallback link to make sure the donations block |
| 70 |
// points to the correct post when it's inserted from the synced pattern (aka “My Pattern”). |
| 71 |
$post_link = get_permalink(); |
| 72 |
$parsed[0]['attrs']['fallbackLinkUrl'] = $post_link; |
| 73 |
$content = \render_block( $parsed[0] ); |
| 74 |
if ( preg_match( '/<a\s+class="jetpack-donations-fallback-link"\s+href="([^"]*)"/', $content, $matches ) ) { |
| 75 |
$content = str_replace( $matches[1], $post_link, $content ); |
| 76 |
} |
| 77 |
} |
| 78 |
|
| 79 |
return $content; |
| 80 |
} |
| 81 |
|
| 82 |
require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php'; |
| 83 |
|
| 84 |
// If stripe isn't connected don't show anything to potential donors - they can't actually make a donation. |
| 85 |
if ( ! \Jetpack_Memberships::has_connected_account() ) { |
| 86 |
return ''; |
| 87 |
} |
| 88 |
|
| 89 |
Jetpack_Gutenberg::load_assets_as_required( __DIR__ ); |
| 90 |
|
| 91 |
require_once JETPACK__PLUGIN_DIR . '/_inc/lib/class-jetpack-currencies.php'; |
| 92 |
|
| 93 |
$default_texts = get_default_texts(); |
| 94 |
|
| 95 |
// `array_merge` lets the user-supplied attributes override only the keys |
| 96 |
// they actually set. Undefined keys (new blocks that have never been |
| 97 |
// edited) fall back to the defaults in the first array. User-cleared |
| 98 |
// keys (empty strings explicitly saved) win over defaults, so |
| 99 |
// "blank stays blank" and "never set" gets the default. |
| 100 |
// Treat `show !== false` as on, so legacy blocks (where `show` was never |
| 101 |
// set on oneTimeDonation) still render the one-time interval by default. |
| 102 |
$donations = array(); |
| 103 |
if ( false !== ( $attr['oneTimeDonation']['show'] ?? true ) ) { |
| 104 |
$donations['one-time'] = array_merge( |
| 105 |
array( |
| 106 |
'planId' => null, |
| 107 |
'title' => __( 'One-Time', 'jetpack' ), |
| 108 |
'class' => 'donations__one-time-item', |
| 109 |
'heading' => $default_texts['oneTimeDonation']['heading'], |
| 110 |
'buttonText' => $default_texts['oneTimeDonation']['buttonText'], |
| 111 |
), |
| 112 |
$attr['oneTimeDonation'] |
| 113 |
); |
| 114 |
} |
| 115 |
if ( $attr['monthlyDonation']['show'] ) { |
| 116 |
$donations['1 month'] = array_merge( |
| 117 |
array( |
| 118 |
'planId' => null, |
| 119 |
'title' => __( 'Monthly', 'jetpack' ), |
| 120 |
'class' => 'donations__monthly-item', |
| 121 |
'heading' => $default_texts['monthlyDonation']['heading'], |
| 122 |
'buttonText' => $default_texts['monthlyDonation']['buttonText'], |
| 123 |
), |
| 124 |
$attr['monthlyDonation'] |
| 125 |
); |
| 126 |
} |
| 127 |
if ( $attr['annualDonation']['show'] ) { |
| 128 |
$donations['1 year'] = array_merge( |
| 129 |
array( |
| 130 |
'planId' => null, |
| 131 |
'title' => __( 'Yearly', 'jetpack' ), |
| 132 |
'class' => 'donations__annual-item', |
| 133 |
'heading' => $default_texts['annualDonation']['heading'], |
| 134 |
'buttonText' => $default_texts['annualDonation']['buttonText'], |
| 135 |
), |
| 136 |
$attr['annualDonation'] |
| 137 |
); |
| 138 |
} |
| 139 |
|
| 140 |
$choose_amount_text = $attr['chooseAmountText'] ?? $default_texts['chooseAmountText']; |
| 141 |
$custom_amount_text = $attr['customAmountText'] ?? $default_texts['customAmountText']; |
| 142 |
$currency = $attr['currency']; |
| 143 |
|
| 144 |
// Drop intervals whose plan no longer resolves so we can compute the active tab |
| 145 |
// against the actually-rendered set, not the configured set. |
| 146 |
$valid_donations = array(); |
| 147 |
foreach ( $donations as $interval => $donation ) { |
| 148 |
$plan = get_post( (int) $donation['planId'] ); |
| 149 |
if ( $plan && ! is_wp_error( $plan ) ) { |
| 150 |
$valid_donations[ $interval ] = $donation; |
| 151 |
} |
| 152 |
} |
| 153 |
$donations = $valid_donations; |
| 154 |
$rendered_intervals = array_keys( $donations ); |
| 155 |
|
| 156 |
// Effective default = configured defaultInterval if it survived plan validation, |
| 157 |
// otherwise the first rendered interval (one-time → monthly → annual). |
| 158 |
$default_interval = $attr['defaultInterval'] ?? null; |
| 159 |
if ( ! in_array( $default_interval, $rendered_intervals, true ) ) { |
| 160 |
$default_interval = $rendered_intervals[0] ?? null; |
| 161 |
} |
| 162 |
$tab_content_class_map = array( |
| 163 |
'one-time' => 'is-one-time', |
| 164 |
'1 month' => 'is-monthly', |
| 165 |
'1 year' => 'is-annual', |
| 166 |
); |
| 167 |
$tab_content_class = $default_interval ? $tab_content_class_map[ $default_interval ] : ''; |
| 168 |
|
| 169 |
$nav = ''; |
| 170 |
$headings = ''; |
| 171 |
$amounts = ''; |
| 172 |
$extra_text = ''; |
| 173 |
$buttons = ''; |
| 174 |
foreach ( $donations as $interval => $donation ) { |
| 175 |
$plan_id = (int) $donation['planId']; |
| 176 |
|
| 177 |
if ( count( $donations ) > 1 ) { |
| 178 |
if ( ! $nav ) { |
| 179 |
$nav .= '<div class="donations__nav">'; |
| 180 |
} |
| 181 |
$is_active_class = $interval === $default_interval ? ' is-active' : ''; |
| 182 |
$nav .= sprintf( |
| 183 |
'<div role="button" tabindex="0" class="donations__nav-item%3$s" data-interval="%1$s">%2$s</div>', |
| 184 |
esc_attr( $interval ), |
| 185 |
esc_html( $donation['title'] ), |
| 186 |
esc_attr( $is_active_class ) |
| 187 |
); |
| 188 |
} |
| 189 |
$heading_text = wp_kses_post( $donation['heading'] ?? '' ); |
| 190 |
if ( '' !== trim( $heading_text ) ) { |
| 191 |
$headings .= sprintf( |
| 192 |
'<h4 class="%1$s">%2$s</h4>', |
| 193 |
esc_attr( $donation['class'] ), |
| 194 |
$heading_text |
| 195 |
); |
| 196 |
} |
| 197 |
$default_index_attr = ''; |
| 198 |
if ( isset( $donation['defaultAmountIndex'] ) && is_numeric( $donation['defaultAmountIndex'] ) ) { |
| 199 |
$default_index_attr = sprintf( ' data-default-index="%d"', (int) $donation['defaultAmountIndex'] ); |
| 200 |
} |
| 201 |
$amounts .= sprintf( |
| 202 |
'<div class="donations__amounts %s"%s>', |
| 203 |
esc_attr( $donation['class'] ), |
| 204 |
$default_index_attr |
| 205 |
); |
| 206 |
foreach ( $donation['amounts'] as $amount ) { |
| 207 |
$amounts .= sprintf( |
| 208 |
'<div class="donations__amount" data-amount="%1$s">%2$s</div>', |
| 209 |
esc_attr( $amount ), |
| 210 |
esc_html( \Jetpack_Currencies::format_price( $amount, $currency ) ) |
| 211 |
); |
| 212 |
} |
| 213 |
$amounts .= '</div>'; |
| 214 |
$extra_text_html = wp_kses_post( $donation['extraText'] ?? $default_texts['extraText'] ); |
| 215 |
if ( '' !== trim( $extra_text_html ) ) { |
| 216 |
$extra_text .= sprintf( |
| 217 |
'<p class="%1$s">%2$s</p>', |
| 218 |
esc_attr( $donation['class'] ), |
| 219 |
$extra_text_html |
| 220 |
); |
| 221 |
} |
| 222 |
$buttons .= sprintf( |
| 223 |
'<div class="wp-block-button donations__donate-button-wrapper %1$s"><a class="wp-block-button__link wp-element-button donations__donate-button %1$s" href="%2$s">%3$s</a></div>', |
| 224 |
esc_attr( $donation['class'] ), |
| 225 |
esc_url( \Jetpack_Memberships::get_instance()->get_subscription_url( $plan_id ) ), |
| 226 |
wp_kses_post( $donation['buttonText'] ) |
| 227 |
); |
| 228 |
} |
| 229 |
if ( $nav ) { |
| 230 |
$nav .= '</div>'; |
| 231 |
} |
| 232 |
|
| 233 |
$custom_amount = ''; |
| 234 |
if ( $attr['showCustomAmount'] ) { |
| 235 |
$custom_amount_html = wp_kses_post( $custom_amount_text ); |
| 236 |
if ( '' !== trim( $custom_amount_html ) ) { |
| 237 |
$custom_amount .= sprintf( '<p>%s</p>', $custom_amount_html ); |
| 238 |
} |
| 239 |
$default_custom_amount = $attr['customAmountPlaceholder'] |
| 240 |
?? ( \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] ?? 1 ) * 100; |
| 241 |
$custom_amount .= sprintf( |
| 242 |
'<div class="donations__amount donations__custom-amount"> |
| 243 |
%1$s |
| 244 |
<div class="donations__amount-value" data-currency="%2$s" data-empty-text="%3$s"></div> |
| 245 |
</div>', |
| 246 |
esc_html( \Jetpack_Currencies::CURRENCIES[ $currency ]['symbol'] ?? '¤' ), |
| 247 |
esc_attr( $currency ), |
| 248 |
esc_attr( \Jetpack_Currencies::format_price( $default_custom_amount, $currency, false ) ) |
| 249 |
); |
| 250 |
} |
| 251 |
|
| 252 |
$display_mode = $attr['displayMode'] ?? 'inline'; |
| 253 |
$instance_id = wp_unique_id( 'jp-donations-' ); |
| 254 |
$instance_classes = $instance_id; |
| 255 |
if ( isset( $attr['tabsAppearance'] ) && 'buttons' === $attr['tabsAppearance'] ) { |
| 256 |
$instance_classes .= ' is-style-buttons'; |
| 257 |
} |
| 258 |
if ( 'modal' === $display_mode ) { |
| 259 |
$instance_classes .= ' is-display-modal'; |
| 260 |
if ( ! empty( $attr['triggerSticky'] ) ) { |
| 261 |
$instance_classes .= ' is-sticky'; |
| 262 |
} |
| 263 |
} |
| 264 |
$wrapper_attr_array = array( 'class' => $instance_classes ); |
| 265 |
if ( $default_interval ) { |
| 266 |
$wrapper_attr_array['data-default-interval'] = $default_interval; |
| 267 |
} |
| 268 |
$wrapper_attr_array = array_merge( $wrapper_attr_array, build_security_data_attrs( $attr, $currency ) ); |
| 269 |
$wrapper_attrs = get_block_wrapper_attributes( $wrapper_attr_array ); |
| 270 |
$custom_styles = build_custom_styles( $attr, '.' . $instance_id ); |
| 271 |
|
| 272 |
$choose_amount_html = wp_kses_post( $choose_amount_text ); |
| 273 |
$choose_amount_block = '' !== trim( $choose_amount_html ) ? '<p>' . $choose_amount_html . '</p>' : ''; |
| 274 |
|
| 275 |
if ( 'modal' === $display_mode ) { |
| 276 |
$trigger_text = $attr['triggerButtonText'] ?? $default_texts['triggerButtonText']; |
| 277 |
$trigger_icon_key = $attr['triggerIcon'] ?? 'heart'; |
| 278 |
$trigger_svg = get_trigger_icon_svg( $trigger_icon_key ); |
| 279 |
$modal_id = $instance_id . '-modal'; |
| 280 |
|
| 281 |
return sprintf( |
| 282 |
' |
| 283 |
<div %1$s>%9$s |
| 284 |
<button |
| 285 |
class="donations__trigger-button wp-block-button__link wp-element-button" |
| 286 |
aria-haspopup="dialog" |
| 287 |
aria-controls="%11$s" |
| 288 |
>%12$s%10$s</button> |
| 289 |
<div |
| 290 |
id="%11$s" |
| 291 |
class="donations__modal-overlay" |
| 292 |
role="dialog" |
| 293 |
aria-modal="true" |
| 294 |
aria-label="%10$s" |
| 295 |
hidden |
| 296 |
> |
| 297 |
<div class="donations__modal-dialog"> |
| 298 |
<button class="donations__modal-close" aria-label="%13$s">✕</button> |
| 299 |
<div class="donations__modal-content"> |
| 300 |
<div class="donations__container"> |
| 301 |
%2$s |
| 302 |
<div class="donations__content"> |
| 303 |
<div class="donations__tab %14$s"> |
| 304 |
%3$s |
| 305 |
%4$s |
| 306 |
%5$s |
| 307 |
%6$s |
| 308 |
<div class="donations__range-error"></div> |
| 309 |
<hr class="donations__separator"> |
| 310 |
%7$s |
| 311 |
%8$s |
| 312 |
</div> |
| 313 |
</div> |
| 314 |
</div> |
| 315 |
</div> |
| 316 |
</div> |
| 317 |
</div> |
| 318 |
</div> |
| 319 |
', |
| 320 |
$wrapper_attrs, |
| 321 |
$nav, |
| 322 |
$headings, |
| 323 |
$choose_amount_block, |
| 324 |
$amounts, |
| 325 |
$custom_amount, |
| 326 |
$extra_text, |
| 327 |
$buttons, |
| 328 |
$custom_styles ? '<style>' . $custom_styles . '</style>' : '', |
| 329 |
esc_html( $trigger_text ), |
| 330 |
esc_attr( $modal_id ), |
| 331 |
$trigger_svg, |
| 332 |
esc_attr__( 'Close', 'jetpack' ), |
| 333 |
esc_attr( $tab_content_class ) |
| 334 |
); |
| 335 |
} |
| 336 |
|
| 337 |
return sprintf( |
| 338 |
' |
| 339 |
<div %1$s>%9$s |
| 340 |
<div class="donations__container"> |
| 341 |
%2$s |
| 342 |
<div class="donations__content"> |
| 343 |
<div class="donations__tab %10$s"> |
| 344 |
%3$s |
| 345 |
%4$s |
| 346 |
%5$s |
| 347 |
%6$s |
| 348 |
<div class="donations__range-error"></div> |
| 349 |
<hr class="donations__separator"> |
| 350 |
%7$s |
| 351 |
%8$s |
| 352 |
</div> |
| 353 |
</div> |
| 354 |
</div> |
| 355 |
</div> |
| 356 |
', |
| 357 |
$wrapper_attrs, |
| 358 |
$nav, |
| 359 |
$headings, |
| 360 |
$choose_amount_block, |
| 361 |
$amounts, |
| 362 |
$custom_amount, |
| 363 |
$extra_text, |
| 364 |
$buttons, |
| 365 |
$custom_styles ? '<style>' . $custom_styles . '</style>' : '', |
| 366 |
esc_attr( $tab_content_class ) |
| 367 |
); |
| 368 |
} |
| 369 |
|
| 370 |
/** |
| 371 |
* WooCommerce Email Editor render callback for the Donations block. |
| 372 |
* |
| 373 |
* Builds a dedicated email version of the block from its attributes (rather than |
| 374 |
* post-processing the interactive frontend markup) so customizations — per |
| 375 |
* interval heading, supporting text and button label — are preserved. Each |
| 376 |
* interval is rendered as an email-safe table section with a CTA button drawn by |
| 377 |
* the Button block's own email renderer. |
| 378 |
* |
| 379 |
* @param string $block_content The rendered block content (unused; we rebuild from attributes). |
| 380 |
* @param array $parsed_block The parsed block data. |
| 381 |
* @param object $rendering_context The email rendering context. |
| 382 |
* |
| 383 |
* @return string |
| 384 |
*/ |
| 385 |
function render_email( $block_content, array $parsed_block, $rendering_context ) { |
| 386 |
if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] ) |
| 387 |
|| ! function_exists( '\Automattic\Jetpack\Extensions\Button\render_email' ) |
| 388 |
|| ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' ) ) { |
| 389 |
return ''; |
| 390 |
} |
| 391 |
|
| 392 |
$attr = $parsed_block['attrs']; |
| 393 |
$default_texts = get_default_texts(); |
| 394 |
|
| 395 |
$url = get_permalink(); |
| 396 |
if ( ! is_string( $url ) || '' === $url ) { |
| 397 |
$url = is_string( $attr['fallbackLinkUrl'] ?? null ) && '' !== $attr['fallbackLinkUrl'] ? $attr['fallbackLinkUrl'] : '#'; |
| 398 |
} |
| 399 |
|
| 400 |
$alignment = in_array( $attr['contentAlignment'] ?? '', array( 'left', 'center', 'right' ), true ) ? $attr['contentAlignment'] : 'left'; |
| 401 |
$table_style = sprintf( 'width:100%%;max-width:%dpx;border-collapse:collapse;', get_email_target_width( $rendering_context ) ); |
| 402 |
|
| 403 |
$sections = ''; |
| 404 |
foreach ( array( |
| 405 |
'oneTimeDonation' => true, |
| 406 |
'monthlyDonation' => false, |
| 407 |
'annualDonation' => false, |
| 408 |
) as $key => $default_show ) { |
| 409 |
$interval = is_array( $attr[ $key ] ?? null ) ? $attr[ $key ] : array(); |
| 410 |
if ( false === ( $interval['show'] ?? $default_show ) ) { |
| 411 |
continue; |
| 412 |
} |
| 413 |
|
| 414 |
$heading = wp_kses_post( $interval['heading'] ?? $default_texts[ $key ]['heading'] ); |
| 415 |
$extra = wp_kses_post( $interval['extraText'] ?? $default_texts['extraText'] ); |
| 416 |
|
| 417 |
$content = ''; |
| 418 |
if ( '' !== trim( wp_strip_all_tags( $heading ) ) ) { |
| 419 |
$content .= sprintf( |
| 420 |
'<p style="margin:0 0 4px;font-size:18px;font-weight:700;line-height:1.3;text-align:%s;">%s</p>', |
| 421 |
esc_attr( $alignment ), |
| 422 |
$heading |
| 423 |
); |
| 424 |
} |
| 425 |
if ( '' !== trim( wp_strip_all_tags( $extra ) ) ) { |
| 426 |
$content .= sprintf( |
| 427 |
'<p style="margin:0 0 14px;font-size:15px;line-height:1.5;text-align:%s;">%s</p>', |
| 428 |
esc_attr( $alignment ), |
| 429 |
$extra |
| 430 |
); |
| 431 |
} |
| 432 |
$content .= render_email_donate_button( $interval['buttonText'] ?? $default_texts[ $key ]['buttonText'], $url, $attr, $rendering_context ); |
| 433 |
|
| 434 |
$is_first = '' === $sections; |
| 435 |
$cell_style = sprintf( |
| 436 |
'text-align:%1$s;padding:%2$s;%3$s', |
| 437 |
esc_attr( $alignment ), |
| 438 |
$is_first ? '0 0 24px' : '24px 0', |
| 439 |
$is_first ? '' : 'border-top:1px solid #e0e0e0;' |
| 440 |
); |
| 441 |
$sections .= \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper( |
| 442 |
$content, |
| 443 |
array( 'style' => $table_style ), |
| 444 |
array( 'style' => $cell_style ) |
| 445 |
); |
| 446 |
} |
| 447 |
|
| 448 |
return $sections; |
| 449 |
} |
| 450 |
|
| 451 |
/** |
| 452 |
* Render a single donation CTA as an email-safe button via the Button block's |
| 453 |
* email renderer, carrying over the donation block's button styling. |
| 454 |
* |
| 455 |
* @param string $text Button label. |
| 456 |
* @param string $url Button destination. |
| 457 |
* @param array $attr Donations block attributes. |
| 458 |
* @param object $rendering_context The email rendering context. |
| 459 |
* @return string |
| 460 |
*/ |
| 461 |
function render_email_donate_button( $text, $url, $attr, $rendering_context ) { |
| 462 |
$text = trim( wp_strip_all_tags( (string) $text ) ); |
| 463 |
|
| 464 |
$button_attrs = array( |
| 465 |
'text' => '' !== $text ? $text : __( 'Donate', 'jetpack' ), |
| 466 |
'url' => $url, |
| 467 |
); |
| 468 |
|
| 469 |
if ( ! empty( $attr['buttonFontSize'] ) && is_string( $attr['buttonFontSize'] ) ) { |
| 470 |
$button_attrs['customFontSize'] = $attr['buttonFontSize']; |
| 471 |
} |
| 472 |
if ( isset( $attr['buttonBorderRadius'] ) && is_string( $attr['buttonBorderRadius'] ) ) { |
| 473 |
$radius = (int) $attr['buttonBorderRadius']; |
| 474 |
if ( $radius > 0 ) { |
| 475 |
$button_attrs['borderRadius'] = $radius; |
| 476 |
} |
| 477 |
} |
| 478 |
|
| 479 |
return \Automattic\Jetpack\Extensions\Button\render_email( |
| 480 |
'', |
| 481 |
array( 'attrs' => $button_attrs ), |
| 482 |
$rendering_context |
| 483 |
); |
| 484 |
} |
| 485 |
|
| 486 |
/** |
| 487 |
* Resolve the target content width (in px) from the email rendering context, |
| 488 |
* falling back to a sensible default. |
| 489 |
* |
| 490 |
* @param object $rendering_context The email rendering context. |
| 491 |
* @return int Width in pixels. |
| 492 |
*/ |
| 493 |
function get_email_target_width( $rendering_context ) { |
| 494 |
$target_width = 600; |
| 495 |
if ( is_object( $rendering_context ) && method_exists( $rendering_context, 'get_layout_width_without_padding' ) ) { |
| 496 |
$width = $rendering_context->get_layout_width_without_padding(); |
| 497 |
if ( is_string( $width ) && preg_match( '/(\d+)px/', $width, $matches ) ) { |
| 498 |
$parsed = (int) $matches[1]; |
| 499 |
if ( $parsed > 0 ) { |
| 500 |
$target_width = $parsed; |
| 501 |
} |
| 502 |
} |
| 503 |
} |
| 504 |
return $target_width; |
| 505 |
} |
| 506 |
|
| 507 |
/** |
| 508 |
* Build data-attributes array for security (min/max amount) constraints. |
| 509 |
* |
| 510 |
* Extracted so it can be tested independently of the full render pipeline. |
| 511 |
* |
| 512 |
* @since 15.9 |
| 513 |
* |
| 514 |
* @param array $attr Block attributes. |
| 515 |
* @param string $currency Currency code (e.g. 'USD'). |
| 516 |
* @return array Associative array of data-attribute name => value. |
| 517 |
*/ |
| 518 |
function build_security_data_attrs( $attr, $currency ) { |
| 519 |
$attrs = array(); |
| 520 |
$min_amount = isset( $attr['minimumAmount'] ) ? (float) $attr['minimumAmount'] : null; |
| 521 |
$max_amount = isset( $attr['maximumAmount'] ) ? (float) $attr['maximumAmount'] : null; |
| 522 |
if ( null !== $min_amount ) { |
| 523 |
$attrs['data-min-amount'] = $min_amount; |
| 524 |
$attrs['data-min-error'] = sprintf( |
| 525 |
/* translators: %s: minimum donation amount formatted with currency symbol */ |
| 526 |
__( 'The minimum donation amount is %s.', 'jetpack' ), |
| 527 |
\Jetpack_Currencies::format_price( (string) $min_amount, $currency ) |
| 528 |
); |
| 529 |
} |
| 530 |
if ( null !== $max_amount ) { |
| 531 |
$attrs['data-max-amount'] = $max_amount; |
| 532 |
$attrs['data-max-error'] = sprintf( |
| 533 |
/* translators: %s: maximum donation amount formatted with currency symbol */ |
| 534 |
__( 'The maximum donation amount is %s.', 'jetpack' ), |
| 535 |
\Jetpack_Currencies::format_price( (string) $max_amount, $currency ) |
| 536 |
); |
| 537 |
} |
| 538 |
$stripe_min = \Jetpack_Memberships::SUPPORTED_CURRENCIES[ $currency ] ?? 1; |
| 539 |
$attrs['data-stripe-min-error'] = sprintf( |
| 540 |
/* translators: %s: payment processor minimum donation amount formatted with currency symbol */ |
| 541 |
_x( 'The minimum donation amount is %s.', 'payment processor minimum', 'jetpack' ), |
| 542 |
\Jetpack_Currencies::format_price( (string) $stripe_min, $currency ) |
| 543 |
); |
| 544 |
return $attrs; |
| 545 |
} |
| 546 |
|
| 547 |
/** |
| 548 |
* Build a CSS string scoping per-state and tab-level style rules to a single |
| 549 |
* block instance. |
| 550 |
* |
| 551 |
* @param array $attr Block attributes. |
| 552 |
* @param string $scope CSS class selector (with leading dot) unique to this instance. |
| 553 |
* @return string CSS rules joined into one string, or '' when no overrides are set. |
| 554 |
*/ |
| 555 |
function build_custom_styles( $attr, $scope ) { |
| 556 |
$tab_padding = isset( $attr['tabPadding'] ) && is_array( $attr['tabPadding'] ) ? $attr['tabPadding'] : array(); |
| 557 |
$button_padding = isset( $attr['buttonPadding'] ) && is_array( $attr['buttonPadding'] ) ? $attr['buttonPadding'] : array(); |
| 558 |
|
| 559 |
$groups = array( |
| 560 |
array( |
| 561 |
'selector' => $scope . ' .donations__nav-item', |
| 562 |
'properties' => array( |
| 563 |
'font-size' => $attr['tabFontSize'] ?? '', |
| 564 |
'padding-top' => $tab_padding['top'] ?? '', |
| 565 |
'padding-right' => $tab_padding['right'] ?? '', |
| 566 |
'padding-bottom' => $tab_padding['bottom'] ?? '', |
| 567 |
'padding-left' => $tab_padding['left'] ?? '', |
| 568 |
), |
| 569 |
), |
| 570 |
array( |
| 571 |
'selector' => $scope . ' .donations__nav-item.is-active', |
| 572 |
'properties' => array( |
| 573 |
'background' => $attr['activeTabBackgroundColor'] ?? '', |
| 574 |
'color' => $attr['activeTabTextColor'] ?? '', |
| 575 |
), |
| 576 |
), |
| 577 |
array( |
| 578 |
'selector' => $scope . ' .donations__nav-item:not(.is-active)', |
| 579 |
'properties' => array( |
| 580 |
'background' => $attr['inactiveTabBackgroundColor'] ?? '', |
| 581 |
'color' => $attr['inactiveTabTextColor'] ?? '', |
| 582 |
), |
| 583 |
), |
| 584 |
array( |
| 585 |
'selector' => $scope . ' .donations__amount.is-selected', |
| 586 |
'properties' => array( |
| 587 |
'background-color' => $attr['selectedAmountBackgroundColor'] ?? '', |
| 588 |
'color' => $attr['selectedAmountTextColor'] ?? '', |
| 589 |
), |
| 590 |
), |
| 591 |
array( |
| 592 |
'selector' => $scope . ' .donations__amount.is-selected', |
| 593 |
'properties' => array( |
| 594 |
// Override only the outer ring color; the inner 1px white separator stays put. |
| 595 |
'box-shadow' => isset( $attr['selectedAmountOutlineColor'] ) && '' !== $attr['selectedAmountOutlineColor'] |
| 596 |
? '0 0 0 1px #fff,0 0 0 3px ' . $attr['selectedAmountOutlineColor'] |
| 597 |
: '', |
| 598 |
), |
| 599 |
), |
| 600 |
array( |
| 601 |
'selector' => $scope . ' .donations__donate-button', |
| 602 |
'properties' => array( |
| 603 |
'font-size' => $attr['buttonFontSize'] ?? '', |
| 604 |
'padding-top' => $button_padding['top'] ?? '', |
| 605 |
'padding-right' => $button_padding['right'] ?? '', |
| 606 |
'padding-bottom' => $button_padding['bottom'] ?? '', |
| 607 |
'padding-left' => $button_padding['left'] ?? '', |
| 608 |
), |
| 609 |
), |
| 610 |
); |
| 611 |
|
| 612 |
$rules = array(); |
| 613 |
|
| 614 |
$content_alignment = $attr['contentAlignment'] ?? ''; |
| 615 |
if ( in_array( $content_alignment, array( 'left', 'center', 'right' ), true ) ) { |
| 616 |
$rules[] = $scope . ' .donations__content{text-align:' . $content_alignment . '}'; |
| 617 |
$justify_map = array( |
| 618 |
'left' => 'flex-start', |
| 619 |
'center' => 'center', |
| 620 |
'right' => 'flex-end', |
| 621 |
); |
| 622 |
$rules[] = $scope . ' .donations__amounts{justify-content:' . $justify_map[ $content_alignment ] . '}'; |
| 623 |
} |
| 624 |
|
| 625 |
foreach ( $groups as $group ) { |
| 626 |
$decls = array(); |
| 627 |
foreach ( $group['properties'] as $property => $value ) { |
| 628 |
$safe = sanitize_css_value( $value ); |
| 629 |
if ( '' !== $safe ) { |
| 630 |
$decls[] = $property . ':' . $safe; |
| 631 |
} |
| 632 |
} |
| 633 |
if ( $decls ) { |
| 634 |
$rules[] = $group['selector'] . '{' . implode( ';', $decls ) . '}'; |
| 635 |
} |
| 636 |
} |
| 637 |
|
| 638 |
// User-set tab border color: applies to the default-style nav bottom |
| 639 |
// divider, the per-tab dividers, and the buttons-style pill borders. |
| 640 |
$tab_border_safe = sanitize_css_value( $attr['tabBorderColor'] ?? '' ); |
| 641 |
if ( '' !== $tab_border_safe ) { |
| 642 |
$rules[] = $scope . ' .donations__nav,' . $scope . ' .donations__nav-item,' . $scope . ' .donations__nav-item.is-active{border-color:' . $tab_border_safe . '}'; |
| 643 |
} |
| 644 |
|
| 645 |
$button_radius_decls = build_radius_decls( $attr['buttonBorderRadius'] ?? null ); |
| 646 |
if ( $button_radius_decls ) { |
| 647 |
$rules[] = $scope . ' .donations__donate-button{' . implode( ';', $button_radius_decls ) . '}'; |
| 648 |
} |
| 649 |
|
| 650 |
// User-set amount tile font size, border (BorderBoxControl shape) and |
| 651 |
// border radius (BorderRadiusControl shape). Applies to all amount tiles |
| 652 |
// (preset + custom); selected-state colors above only kick in when an |
| 653 |
// amount has the is-selected class. |
| 654 |
$amount_decls = array(); |
| 655 |
$amount_font = sanitize_css_value( $attr['amountFontSize'] ?? '' ); |
| 656 |
if ( '' !== $amount_font ) { |
| 657 |
$amount_decls[] = 'font-size:' . $amount_font; |
| 658 |
} |
| 659 |
$amount_decls = array_merge( $amount_decls, build_border_decls( $attr['amountBorder'] ?? null ) ); |
| 660 |
$amount_decls = array_merge( $amount_decls, build_radius_decls( $attr['amountBorderRadius'] ?? null ) ); |
| 661 |
if ( $amount_decls ) { |
| 662 |
$rules[] = $scope . ' .donations__amount{' . implode( ';', $amount_decls ) . '}'; |
| 663 |
} |
| 664 |
|
| 665 |
$button_alignment = $attr['buttonAlignment'] ?? ''; |
| 666 |
if ( in_array( $button_alignment, array( 'left', 'center', 'right' ), true ) ) { |
| 667 |
$rules[] = $scope . ' .donations__donate-button-wrapper{text-align:' . $button_alignment . '}'; |
| 668 |
} elseif ( 'full' === $button_alignment ) { |
| 669 |
$rules[] = $scope . ' .donations__donate-button-wrapper{display:block;width:100%}' |
| 670 |
. $scope . ' .donations__donate-button{display:block;width:100%;box-sizing:border-box;text-align:center}'; |
| 671 |
} |
| 672 |
|
| 673 |
// Pop-up display mode: wire up trigger-button alignment via text-align on |
| 674 |
// the wrapper so the inline-flex button responds to the alignment toolbar. |
| 675 |
if ( 'modal' === ( $attr['displayMode'] ?? 'inline' ) ) { |
| 676 |
if ( in_array( $content_alignment, array( 'left', 'center', 'right' ), true ) ) { |
| 677 |
$rules[] = $scope . '{text-align:' . $content_alignment . '}'; |
| 678 |
} |
| 679 |
} |
| 680 |
|
| 681 |
// Wrapper border (In-page mode only). Compound selector (.wp-block-jetpack-donations.jp-donations-N) |
| 682 |
// has specificity 0,2,0 and wins over the default single-class rule (0,1,0) in common.scss. |
| 683 |
if ( 'modal' !== ( $attr['displayMode'] ?? 'inline' ) ) { |
| 684 |
$wrapper_border_decls = array_merge( |
| 685 |
build_border_decls( $attr['blockBorder'] ?? null ), |
| 686 |
build_radius_decls( $attr['blockBorderRadius'] ?? null ) |
| 687 |
); |
| 688 |
if ( $wrapper_border_decls ) { |
| 689 |
$rules[] = '.wp-block-jetpack-donations' . $scope . '{' . implode( ';', $wrapper_border_decls ) . '}'; |
| 690 |
} |
| 691 |
} |
| 692 |
|
| 693 |
return implode( '', $rules ); |
| 694 |
} |
| 695 |
|
| 696 |
/** |
| 697 |
* Convert a uniform-or-split BorderBoxControl value into individual CSS declarations. |
| 698 |
* Uniform shape: { color, style, width }. Split shape: { top: {...}, right: ..., etc. }. |
| 699 |
* |
| 700 |
* @param mixed $border BorderBoxControl value (or null). |
| 701 |
* @return array List of CSS declaration strings (e.g. "border-color:#abc"), already sanitized. |
| 702 |
*/ |
| 703 |
function build_border_decls( $border ) { |
| 704 |
if ( ! is_array( $border ) ) { |
| 705 |
return array(); |
| 706 |
} |
| 707 |
$decls = array(); |
| 708 |
$sides = array( 'top', 'right', 'bottom', 'left' ); |
| 709 |
$is_split = false; |
| 710 |
foreach ( $sides as $side ) { |
| 711 |
if ( isset( $border[ $side ] ) ) { |
| 712 |
$is_split = true; |
| 713 |
break; |
| 714 |
} |
| 715 |
} |
| 716 |
if ( $is_split ) { |
| 717 |
foreach ( $sides as $side ) { |
| 718 |
$sb = $border[ $side ] ?? null; |
| 719 |
if ( ! is_array( $sb ) ) { |
| 720 |
continue; |
| 721 |
} |
| 722 |
foreach ( array( 'color', 'style', 'width' ) as $prop ) { |
| 723 |
$safe = sanitize_css_value( $sb[ $prop ] ?? '' ); |
| 724 |
if ( '' !== $safe ) { |
| 725 |
$decls[] = 'border-' . $side . '-' . $prop . ':' . $safe; |
| 726 |
} |
| 727 |
} |
| 728 |
} |
| 729 |
} else { |
| 730 |
foreach ( array( 'color', 'style', 'width' ) as $prop ) { |
| 731 |
$safe = sanitize_css_value( $border[ $prop ] ?? '' ); |
| 732 |
if ( '' !== $safe ) { |
| 733 |
$decls[] = 'border-' . $prop . ':' . $safe; |
| 734 |
} |
| 735 |
} |
| 736 |
} |
| 737 |
return $decls; |
| 738 |
} |
| 739 |
|
| 740 |
/** |
| 741 |
* Convert a uniform-or-per-corner BorderRadiusControl value into CSS declarations. |
| 742 |
* Uniform shape: a string like "8px". Per-corner shape: |
| 743 |
* { topLeft, topRight, bottomRight, bottomLeft } each with string values. |
| 744 |
* |
| 745 |
* @param mixed $radius BorderRadiusControl value (or null). |
| 746 |
* @return array List of CSS declaration strings. |
| 747 |
*/ |
| 748 |
function build_radius_decls( $radius ) { |
| 749 |
if ( is_string( $radius ) && '' !== $radius ) { |
| 750 |
$safe = sanitize_css_value( $radius ); |
| 751 |
return '' !== $safe ? array( 'border-radius:' . $safe ) : array(); |
| 752 |
} |
| 753 |
if ( ! is_array( $radius ) ) { |
| 754 |
return array(); |
| 755 |
} |
| 756 |
$corners = array( |
| 757 |
'topLeft' => 'border-top-left-radius', |
| 758 |
'topRight' => 'border-top-right-radius', |
| 759 |
'bottomRight' => 'border-bottom-right-radius', |
| 760 |
'bottomLeft' => 'border-bottom-left-radius', |
| 761 |
); |
| 762 |
$decls = array(); |
| 763 |
foreach ( $corners as $key => $css_prop ) { |
| 764 |
$safe = sanitize_css_value( $radius[ $key ] ?? '' ); |
| 765 |
if ( '' !== $safe ) { |
| 766 |
$decls[] = $css_prop . ':' . $safe; |
| 767 |
} |
| 768 |
} |
| 769 |
return $decls; |
| 770 |
} |
| 771 |
|
| 772 |
/** |
| 773 |
* Sanitize a user-supplied CSS value (color, length, etc.) for safe inclusion |
| 774 |
* in a <style> element. Strips characters that could break out of the style |
| 775 |
* context (<, >, {, }, ;, quotes, backslash) and caps length, while leaving |
| 776 |
* valid hex / rgb() / hsl() / var() / named-color / px / rem / em values intact. |
| 777 |
* |
| 778 |
* @param mixed $value Raw attribute value. |
| 779 |
* @return string Sanitized value, or '' if rejected. |
| 780 |
*/ |
| 781 |
function sanitize_css_value( $value ) { |
| 782 |
if ( ! is_string( $value ) || '' === $value ) { |
| 783 |
return ''; |
| 784 |
} |
| 785 |
$value = trim( $value ); |
| 786 |
if ( strlen( $value ) > 100 ) { |
| 787 |
return ''; |
| 788 |
} |
| 789 |
if ( preg_match( '/[<>{};\\\\\'"]/', $value ) ) { |
| 790 |
return ''; |
| 791 |
} |
| 792 |
return $value; |
| 793 |
} |
| 794 |
|
| 795 |
/** |
| 796 |
* Get the default texts for the block. |
| 797 |
* |
| 798 |
* @return array |
| 799 |
*/ |
| 800 |
function get_default_texts() { |
| 801 |
return array( |
| 802 |
'chooseAmountText' => __( 'Choose an amount', 'jetpack' ), |
| 803 |
'customAmountText' => __( 'Or enter a custom amount', 'jetpack' ), |
| 804 |
'extraText' => __( 'Your contribution is appreciated.', 'jetpack' ), |
| 805 |
'triggerButtonText' => __( 'Donate', 'jetpack' ), |
| 806 |
'oneTimeDonation' => array( |
| 807 |
'heading' => __( 'Make a one-time donation', 'jetpack' ), |
| 808 |
'buttonText' => __( 'Donate', 'jetpack' ), |
| 809 |
), |
| 810 |
'monthlyDonation' => array( |
| 811 |
'heading' => __( 'Make a monthly donation', 'jetpack' ), |
| 812 |
'buttonText' => __( 'Donate monthly', 'jetpack' ), |
| 813 |
), |
| 814 |
'annualDonation' => array( |
| 815 |
'heading' => __( 'Make a yearly donation', 'jetpack' ), |
| 816 |
'buttonText' => __( 'Donate yearly', 'jetpack' ), |
| 817 |
), |
| 818 |
); |
| 819 |
} |
| 820 |
|
| 821 |
/** |
| 822 |
* Return inline SVG markup for a trigger button icon. |
| 823 |
* |
| 824 |
* Path data sourced from icons.js ICON_SVG_PATHS — keep in sync. |
| 825 |
* |
| 826 |
* @param string $icon_key Icon key (e.g. 'coffee', 'heart', 'gift'). |
| 827 |
* @return string SVG HTML string, or '' when key is 'none' or unknown. |
| 828 |
*/ |
| 829 |
function get_trigger_icon_svg( $icon_key ) { |
| 830 |
$paths = array( |
| 831 |
'heart' => array( 'd' => 'M16.5 4.5c2.206 0 4 1.794 4 4 0 4.67-5.543 8.94-8.5 11.023C9.043 17.44 3.5 13.17 3.5 8.5c0-2.206 1.794-4 4-4 1.298 0 2.522.638 3.273 1.706L12 7.953l1.227-1.746c.75-1.07 1.975-1.707 3.273-1.707m0-1.5c-1.862 0-3.505.928-4.5 2.344C11.005 3.928 9.362 3 7.5 3 4.462 3 2 5.462 2 8.5c0 5.72 6.5 10.438 10 12.85 3.5-2.412 10-7.13 10-12.85C22 5.462 19.538 3 16.5 3z' ), |
| 832 |
'gift' => array( 'd' => 'M15.333 4C16.6677 4 17.75 5.0823 17.75 6.41699V6.75C17.75 7.20058 17.6394 7.62468 17.4473 8H18.5C19.2767 8 19.9154 8.59028 19.9922 9.34668L20 9.5V18.5C20 19.3284 19.3284 20 18.5 20H5.5C4.72334 20 4.08461 19.4097 4.00781 18.6533L4 18.5V9.5L4.00781 9.34668C4.07949 8.64069 4.64069 8.07949 5.34668 8.00781L5.5 8H6.55273C6.36065 7.62468 6.25 7.20058 6.25 6.75V6.41699C6.25 5.0823 7.3323 4 8.66699 4C10.0436 4.00011 11.2604 4.68183 12 5.72559C12.7396 4.68183 13.9564 4.00011 15.333 4ZM5.5 18.5H11.25V9.5H5.5V18.5ZM12.75 18.5H18.5V9.5H12.75V18.5ZM8.66699 5.5C8.16073 5.5 7.75 5.91073 7.75 6.41699V6.75C7.75 7.44036 8.30964 8 9 8H11.2461C11.2021 6.61198 10.0657 5.50017 8.66699 5.5ZM15.333 5.5C13.9343 5.50017 12.7979 6.61198 12.7539 8H15C15.6904 8 16.25 7.44036 16.25 6.75V6.41699C16.25 5.91073 15.8393 5.5 15.333 5.5Z' ), |
| 833 |
'star' => array( 'd' => 'M11.776 4.454a.25.25 0 01.448 0l2.069 4.192a.25.25 0 00.188.137l4.626.672a.25.25 0 01.139.426l-3.348 3.263a.25.25 0 00-.072.222l.79 4.607a.25.25 0 01-.362.263l-4.138-2.175a.25.25 0 00-.232 0l-4.138 2.175a.25.25 0 01-.363-.263l.79-4.607a.25.25 0 00-.071-.222L4.754 9.881a.25.25 0 01.139-.426l4.626-.672a.25.25 0 00.188-.137l2.069-4.192z' ), |
| 834 |
'thumbs-up' => array( 'd' => 'm3 12 1 8h1.5l-1-8H3Zm15.8-2h-4.4l.8-3.6c.3-1.3-.7-2.4-1.9-2.4h-.2c-.6 0-1.2.3-1.6.8l-5 6.6c-.3.4-.4.8-.4 1.2v.2l.7 5.4v.2c.2.9 1 1.5 1.9 1.5h8.2c.9 0 1.7-.6 1.9-1.4l1.8-6c.4-1.3-.6-2.6-1.9-2.6Zm.5 2.1-1.8 6c0 .2-.3.4-.5.4H8.8c-.3 0-.5-.2-.5-.4l-.7-5.4v-.4l5-6.6c0-.1.2-.2.4-.2h.2c.3 0 .6.3.5.6l-.8 3.6c-.1.4 0 .9.3 1.3s.7.6 1.2.6h4.4c.3 0 .6.3.5.6Z' ), |
| 835 |
'smiley' => array( 'd' => 'M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5 14.67 11 15.5 11zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z' ), |
| 836 |
'coffee' => array( |
| 837 |
'd' => 'M6.5 5h11v3.25h1a2 2 0 010 4H17.5v1.25a3 3 0 01-3 3H9.5a3 3 0 01-3-3V5zm1.5 1.5v7.25a1.5 1.5 0 001.5 1.5h5a1.5 1.5 0 001.5-1.5V6.5h-8zm10 2.5H17.5v2h.5a1 1 0 000-2zM5 20h14V18.5H5z', |
| 838 |
'fill-rule' => 'evenodd', |
| 839 |
), |
| 840 |
'tip-jar' => array( 'd' => 'M9 3h6c0-1.1-.9-2-2-2H11C9.9 1 9 1.9 9 3zm7 0H8c-.55 0-1 .45-1 1v1.5c0 .55.45 1 1 1h8c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zm-1 3.5H9V19c0 1.1.9 2 2 2h2c1.1 0 2-.9 2-2V6.5zm-3 1.5h2v1.5l-1 1.5-1-1.5V8z' ), |
| 841 |
'hand-heart' => array( 'd' => 'M15.5 2.1c-1.1 0-2 .6-2.5 1.4-.5-.9-1.4-1.4-2.5-1.4C8.8 2.1 7.5 3.4 7.5 5c0 2.5 4.5 5.9 5.5 6.6 1-.7 5.5-4.1 5.5-6.6 0-1.6-1.3-2.9-3-2.9zM9 14H7l-2 7h14l-2-7h-2l-1 3H10l-1-3z' ), |
| 842 |
'people' => array( |
| 843 |
'd' => 'M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z', |
| 844 |
'fill-rule' => 'evenodd', |
| 845 |
), |
| 846 |
); |
| 847 |
|
| 848 |
if ( ! isset( $paths[ $icon_key ] ) || 'none' === $icon_key ) { |
| 849 |
return ''; |
| 850 |
} |
| 851 |
|
| 852 |
$icon = $paths[ $icon_key ]; |
| 853 |
$extra_attrs = isset( $icon['fill-rule'] ) ? ' fill-rule="' . esc_attr( $icon['fill-rule'] ) . '"' : ''; |
| 854 |
|
| 855 |
return sprintf( |
| 856 |
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false" class="donations__trigger-icon"><path d="%s"%s/></svg>', |
| 857 |
esc_attr( $icon['d'] ), |
| 858 |
$extra_attrs |
| 859 |
); |
| 860 |
} |
| 861 |
|
| 862 |
/** |
| 863 |
* Make default texts available to the editor. |
| 864 |
*/ |
| 865 |
function load_editor_scripts() { |
| 866 |
// Only relevant to the editor right now. |
| 867 |
if ( ! is_admin() ) { |
| 868 |
return; |
| 869 |
} |
| 870 |
|
| 871 |
$data = array( |
| 872 |
'defaultTexts' => get_default_texts(), |
| 873 |
); |
| 874 |
|
| 875 |
wp_add_inline_script( |
| 876 |
'jetpack-blocks-editor', |
| 877 |
'var Jetpack_DonationsBlock = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';', |
| 878 |
'before' |
| 879 |
); |
| 880 |
} |
| 881 |
add_action( 'enqueue_block_assets', __NAMESPACE__ . '\load_editor_scripts', 11 ); |
| 882 |
|
| 883 |
/** |
| 884 |
* Determine if AMP should be disabled on posts having Donations blocks. |
| 885 |
* |
| 886 |
* @param bool $skip Skipped. |
| 887 |
* @param int $post_id Post ID. |
| 888 |
* @param WP_Post $post Post. |
| 889 |
* |
| 890 |
* @return bool Whether to skip the post from AMP. |
| 891 |
*/ |
| 892 |
function amp_skip_post( $skip, $post_id, $post ) { |
| 893 |
// When AMP is on standard mode, there are no non-AMP posts to link to where the donation can be completed, so let's |
| 894 |
// prevent the post from being available in AMP. |
| 895 |
if ( function_exists( 'amp_is_canonical' ) && \amp_is_canonical() && has_block( Blocks::get_block_name( __DIR__ ), $post->post_content ) ) { |
| 896 |
return true; |
| 897 |
} |
| 898 |
return $skip; |
| 899 |
} |
| 900 |
add_filter( 'amp_skip_post', __NAMESPACE__ . '\amp_skip_post', 10, 3 ); |
| 901 |
|
| 902 |
/** |
| 903 |
* Record a Tracks event when a post containing the Donations block transitions |
| 904 |
* to "publish" status. Fires once per donations block in the post, with the |
| 905 |
* block's configuration snapshot as event properties. |
| 906 |
* |
| 907 |
* Admin-driven event only (no donor data). Does not fire on regular updates of |
| 908 |
* an already-published post, on autosaves, or on revisions. |
| 909 |
* |
| 910 |
* @since 15.9 |
| 911 |
* |
| 912 |
* @param string $new_status New post status. |
| 913 |
* @param string $old_status Previous post status. |
| 914 |
* @param WP_Post $post Post being transitioned. |
| 915 |
* @return void |
| 916 |
*/ |
| 917 |
function record_block_published_event( $new_status, $old_status, $post ) { |
| 918 |
if ( 'publish' !== $new_status || 'publish' === $old_status ) { |
| 919 |
return; |
| 920 |
} |
| 921 |
if ( ! $post instanceof WP_Post ) { |
| 922 |
return; |
| 923 |
} |
| 924 |
if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) { |
| 925 |
return; |
| 926 |
} |
| 927 |
$block_name = Blocks::get_block_name( __DIR__ ); |
| 928 |
if ( ! has_block( $block_name, $post ) ) { |
| 929 |
return; |
| 930 |
} |
| 931 |
|
| 932 |
$donation_blocks = collect_donation_blocks( parse_blocks( $post->post_content ), $block_name ); |
| 933 |
if ( empty( $donation_blocks ) ) { |
| 934 |
return; |
| 935 |
} |
| 936 |
|
| 937 |
require_once JETPACK__PLUGIN_DIR . 'modules/memberships/class-jetpack-memberships.php'; |
| 938 |
$stripe_connected = \Jetpack_Memberships::has_connected_account(); |
| 939 |
$block_count = count( $donation_blocks ); |
| 940 |
|
| 941 |
foreach ( $donation_blocks as $index => $block ) { |
| 942 |
$props = build_block_published_event_props( $block, $post, $index, $block_count, $stripe_connected ); |
| 943 |
record_published_event( 'donations_block_published', $props ); |
| 944 |
} |
| 945 |
} |
| 946 |
add_action( 'transition_post_status', __NAMESPACE__ . '\record_block_published_event', 10, 3 ); |
| 947 |
|
| 948 |
/** |
| 949 |
* Walk a parsed block tree and return a flat list of Donations blocks (including |
| 950 |
* nested ones inside columns / groups / patterns). |
| 951 |
* |
| 952 |
* @since 15.9 |
| 953 |
* |
| 954 |
* @param array $blocks Parsed blocks from parse_blocks(). |
| 955 |
* @param string $block_name Donations block name (e.g. 'jetpack/donations'). |
| 956 |
* @return array List of parsed blocks matching $block_name. |
| 957 |
*/ |
| 958 |
function collect_donation_blocks( $blocks, $block_name ) { |
| 959 |
$matches = array(); |
| 960 |
foreach ( $blocks as $block ) { |
| 961 |
if ( isset( $block['blockName'] ) && $block_name === $block['blockName'] ) { |
| 962 |
$matches[] = $block; |
| 963 |
} |
| 964 |
if ( ! empty( $block['innerBlocks'] ) ) { |
| 965 |
$matches = array_merge( $matches, collect_donation_blocks( $block['innerBlocks'], $block_name ) ); |
| 966 |
} |
| 967 |
} |
| 968 |
return $matches; |
| 969 |
} |
| 970 |
|
| 971 |
/** |
| 972 |
* Build the property bag for a jetpack_donations_block_published Tracks event. |
| 973 |
* |
| 974 |
* Extracted so it can be tested independently of the WordPress hook plumbing. |
| 975 |
* |
| 976 |
* @since 15.9 |
| 977 |
* |
| 978 |
* @param array $block Parsed block (with 'attrs' key). |
| 979 |
* @param WP_Post $post Post being published. |
| 980 |
* @param int $index 0-based position of this block within the post's donation blocks. |
| 981 |
* @param int $block_count Total count of donations blocks in the post. |
| 982 |
* @param bool $stripe_connected Whether the site currently has a connected Stripe account. |
| 983 |
* @return array Tracks event properties. |
| 984 |
*/ |
| 985 |
function build_block_published_event_props( $block, $post, $index, $block_count, $stripe_connected ) { |
| 986 |
$attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(); |
| 987 |
|
| 988 |
$one_time_show = ! isset( $attrs['oneTimeDonation']['show'] ) || false !== $attrs['oneTimeDonation']['show']; |
| 989 |
$monthly_show = ! empty( $attrs['monthlyDonation']['show'] ); |
| 990 |
$yearly_show = ! empty( $attrs['annualDonation']['show'] ); |
| 991 |
|
| 992 |
$default_interval = $attrs['defaultInterval'] ?? 'one-time'; |
| 993 |
$interval_to_freq = array( |
| 994 |
'one-time' => 'one_time', |
| 995 |
'1 month' => 'monthly', |
| 996 |
'1 year' => 'yearly', |
| 997 |
); |
| 998 |
$default_frequency = $interval_to_freq[ $default_interval ] ?? $default_interval; |
| 999 |
$enabled_count = (int) $one_time_show + (int) $monthly_show + (int) $yearly_show; |
| 1000 |
$tabs_appearance = $attrs['tabsAppearance'] ?? 'tabs'; |
| 1001 |
|
| 1002 |
return array( |
| 1003 |
'feature' => 'donations', |
| 1004 |
'surface' => 'server', |
| 1005 |
'stripe_connected' => (bool) $stripe_connected, |
| 1006 |
'post_id' => (int) $post->ID, |
| 1007 |
'post_type' => (string) $post->post_type, |
| 1008 |
'block_index_in_post' => (int) $index, |
| 1009 |
'block_count_in_post' => (int) $block_count, |
| 1010 |
'currency' => isset( $attrs['currency'] ) ? (string) $attrs['currency'] : null, |
| 1011 |
'default_frequency' => $default_frequency, |
| 1012 |
'enabled_frequencies_count' => $enabled_count, |
| 1013 |
'show_one_time' => $one_time_show, |
| 1014 |
'show_monthly' => $monthly_show, |
| 1015 |
'show_yearly' => $yearly_show, |
| 1016 |
'show_custom_amount' => ! empty( $attrs['showCustomAmount'] ), |
| 1017 |
'has_min_amount' => isset( $attrs['minimumAmount'] ), |
| 1018 |
'has_max_amount' => isset( $attrs['maximumAmount'] ), |
| 1019 |
'has_custom_styles' => has_custom_styles( $attrs ), |
| 1020 |
'tabs_appearance' => (string) $tabs_appearance, |
| 1021 |
); |
| 1022 |
} |
| 1023 |
|
| 1024 |
/** |
| 1025 |
* Detect whether the block carries any of the customizable style attributes |
| 1026 |
* we ship for the RSM project (so we can measure adoption of the new options). |
| 1027 |
* |
| 1028 |
* @since 15.9 |
| 1029 |
* |
| 1030 |
* @param array $attrs Block attributes. |
| 1031 |
* @return bool Whether any style override is set. |
| 1032 |
*/ |
| 1033 |
function has_custom_styles( $attrs ) { |
| 1034 |
$style_keys = array( |
| 1035 |
'tabFontSize', |
| 1036 |
'tabPadding', |
| 1037 |
'activeTabBackgroundColor', |
| 1038 |
'activeTabTextColor', |
| 1039 |
'buttonPadding', |
| 1040 |
); |
| 1041 |
foreach ( $style_keys as $key ) { |
| 1042 |
if ( isset( $attrs[ $key ] ) && '' !== $attrs[ $key ] && array() !== $attrs[ $key ] ) { |
| 1043 |
return true; |
| 1044 |
} |
| 1045 |
} |
| 1046 |
return false; |
| 1047 |
} |
| 1048 |
|
| 1049 |
/** |
| 1050 |
* Send a Donations Tracks event using whichever pipeline is available for the |
| 1051 |
* current environment (WPCOM Simple sites vs. WoA / connected Jetpack sites). |
| 1052 |
* |
| 1053 |
* Mirrors the dual-path pattern used by the Map block. No-op on environments |
| 1054 |
* that have neither path available, so this stays safe on unconnected sites. |
| 1055 |
* |
| 1056 |
* @since 15.9 |
| 1057 |
* |
| 1058 |
* @param string $event_name Event name WITHOUT the `jetpack_` prefix (the Tracking class adds it). |
| 1059 |
* @param array $props Tracks event properties. |
| 1060 |
* @return void |
| 1061 |
*/ |
| 1062 |
function record_published_event( $event_name, $props ) { |
| 1063 |
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { |
| 1064 |
require_lib( 'tracks/client' ); |
| 1065 |
tracks_record_event( wp_get_current_user(), 'jetpack_' . $event_name, $props ); |
| 1066 |
return; |
| 1067 |
} |
| 1068 |
if ( ( new Host() )->is_woa_site() && Jetpack::is_connection_ready() ) { |
| 1069 |
$tracking = new Tracking(); |
| 1070 |
$tracking->record_user_event( $event_name, $props ); |
| 1071 |
} |
| 1072 |
} |
| 1073 |
|