CouponCode.php
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\EmailEditor\Email_Editor_Container;
use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
use Automattic\WooCommerce\EmailEditor\Engine\Theme_Controller;
use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper;
use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper;
use WP_Block;
/**
* CouponCode block for displaying coupon codes in emails.
*
* @since 10.5.0
*/
class CouponCode extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'coupon-code';
/**
* Default styles for the coupon code element.
*/
private const DEFAULT_STYLES = array(
'font-size' => '1.2em',
'padding' => '12px 20px',
'display' => 'inline-block',
'border' => '2px dashed #cccccc',
'border-radius' => '4px',
'box-sizing' => 'border-box',
'color' => '#000000',
'background-color' => '#f5f5f5',
'text-align' => 'center',
'font-weight' => 'bold',
'letter-spacing' => '1px',
);
/**
* Get the editor script handle for this block type.
*
* @param string|null $key Data to get. Valid keys: "handle", "path", "dependencies".
* @return array|string|null
*/
protected function get_block_type_editor_script( $key = null ) {
$script = array(
'handle' => 'wc-' . $this->block_name . '-block',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
'dependencies' => array( 'wc-blocks' ),
);
return null === $key ? $script : ( $script[ $key ] ?? null );
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Render the coupon code block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block|null $block Block instance.
* @return string
*/
protected function render( $attributes, $content, $block ) {
$parsed_block = $block instanceof WP_Block ? $block->parsed_block : array();
$attributes = $this->get_block_attributes( $parsed_block, $attributes );
$coupon_code = $this->get_coupon_code( $attributes );
if ( empty( $coupon_code ) ) {
return '';
}
$rendering_context = $this->get_rendering_context( $block );
$coupon_html = $this->build_coupon_html( $coupon_code, $attributes, $rendering_context );
return $this->wrap_for_email( $coupon_html, $parsed_block );
}
/**
* Get block attributes from parsed block or fallback.
*
* @param array $parsed_block Parsed block data.
* @param array $fallback Fallback attributes.
* @return array
*/
private function get_block_attributes( array $parsed_block, $fallback ): array {
$attributes = $parsed_block['attrs'] ?? $fallback ?? array();
return is_array( $attributes ) ? $attributes : array();
}
/**
* Extract coupon code from attributes.
*
* @param array $attributes Block attributes.
* @return string
*/
private function get_coupon_code( array $attributes ): string {
$coupon_code = $attributes['couponCode'] ?? '';
return is_string( $coupon_code ) ? $coupon_code : '';
}
/**
* Get rendering context from block or create a new one.
*
* @param WP_Block|null $block Block instance.
* @return Rendering_Context
*/
private function get_rendering_context( $block ): Rendering_Context {
if ( $block instanceof WP_Block
&& isset( $block->context['renderingContext'] )
&& $block->context['renderingContext'] instanceof Rendering_Context
) {
return $block->context['renderingContext'];
}
$theme_controller = Email_Editor_Container::container()->get( Theme_Controller::class );
return new Rendering_Context( $theme_controller->get_theme(), array() );
}
/**
* Build the coupon code HTML element with styles.
*
* @param string $coupon_code Coupon code text.
* @param array $attributes Block attributes.
* @param Rendering_Context $rendering_context Rendering context for style resolution.
* @return string
*/
private function build_coupon_html( string $coupon_code, array $attributes, Rendering_Context $rendering_context ): string {
$block_styles = Styles_Helper::get_block_styles(
$attributes,
$rendering_context,
array( 'border', 'background-color', 'color', 'typography', 'spacing' )
);
$declarations = $block_styles['declarations'] ?? array();
if ( ! $this->has_valid_background_color( $declarations ) ) {
$declarations['background-color'] = $this->resolve_background_color( $attributes, $rendering_context );
}
$merged_styles = array_merge( self::DEFAULT_STYLES, $declarations );
$css = \WP_Style_Engine::compile_css( $merged_styles, '' );
return sprintf(
'<span class="woocommerce-coupon-code" style="%s">%s</span>',
esc_attr( $css ),
esc_html( $coupon_code )
);
}
/**
* Check if declarations contain a valid CSS background color.
*
* @param array $declarations CSS declarations.
* @return bool
*/
private function has_valid_background_color( array $declarations ): bool {
if ( empty( $declarations['background-color'] ) ) {
return false;
}
return $this->is_css_color_value( $declarations['background-color'] );
}
/**
* Resolve background color from attributes, translating color slugs if needed.
*
* @param array $attributes Block attributes.
* @param Rendering_Context $rendering_context Rendering context.
* @return string Resolved color value or default.
*/
private function resolve_background_color( array $attributes, Rendering_Context $rendering_context ): string {
if ( empty( $attributes['backgroundColor'] ) ) {
return self::DEFAULT_STYLES['background-color'];
}
$color_slug = $attributes['backgroundColor'];
// Try to get color from normalized styles (handles slug translation).
$normalized = Styles_Helper::get_normalized_block_styles( $attributes, $rendering_context );
$color = $normalized['color']['background'] ?? '';
if ( $this->is_css_color_value( $color ) ) {
return $color;
}
// Fallback: try direct translation if normalization returned the slug unchanged.
$translated = $rendering_context->translate_slug_to_color( $color_slug );
if ( $this->is_css_color_value( $translated ) ) {
return $translated;
}
return self::DEFAULT_STYLES['background-color'];
}
/**
* Check if a string is a valid CSS color value (hex, rgb, or hsl).
*
* @param string $value Value to check.
* @return bool
*/
private function is_css_color_value( string $value ): bool {
return str_starts_with( $value, '#' )
|| str_starts_with( $value, 'rgb' )
|| str_starts_with( $value, 'hsl' );
}
/**
* Wrap coupon HTML in an email-compatible table structure.
*
* @param string $coupon_html Coupon HTML content.
* @param array $parsed_block Parsed block data.
* @return string
*/
private function wrap_for_email( string $coupon_html, array $parsed_block ): string {
$align = $this->get_alignment( $parsed_block );
$table_attrs = array(
'style' => \WP_Style_Engine::compile_css(
array(
'border-collapse' => 'collapse',
'width' => '100%',
),
''
),
'width' => '100%',
);
$cell_attrs = array(
'class' => 'email-coupon-code-cell',
'style' => \WP_Style_Engine::compile_css(
array(
'padding' => '10px 0',
'text-align' => $align,
),
''
),
'align' => $align,
);
return Table_Wrapper_Helper::render_table_wrapper( $coupon_html, $table_attrs, $cell_attrs );
}
/**
* Get alignment from parsed block attributes.
*
* @param array $parsed_block Parsed block data.
* @return string
*/
private function get_alignment( array $parsed_block ): string {
$allowed = array( 'left', 'center', 'right' );
$align = $parsed_block['attrs']['align'] ?? 'center';
if ( ! is_string( $align ) || ! in_array( $align, $allowed, true ) ) {
return 'center';
}
return $align;
}
}