*/
private static $rendered_form_ids = array();
/**
* Get the singleton instance.
*
* Ensures only one instance of Frontend is loaded or can be loaded.
*
* @since 1.0.0
* @static
*
* @return Frontend The single instance of this class.
*/
public static function get_instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*
* Initializes the class and sets up hooks.
*
* @since 1.0.0
* @access private
*/
private function __construct() {
$this->hooks();
}
/**
* Initialize hooks.
*
* @since 1.0.0
* @access private
*/
private function hooks(): void {
// Hook unconditionally to modify Elements config.
add_filter( 'simpay_elements_config', array( $this, 'modify_elements_config' ), 10, 2 );
// Hook to capture rendered form IDs (for inline CSS and for Elements config).
add_action( 'simpay_form_before_form_top', array( $this, 'capture_rendered_form_id' ), 10, 1 ); // Only need form ID.
// Hook late to print inline CSS for non-Stripe elements.
add_action( 'wp_print_footer_scripts', array( $this, 'print_late_frontend_styles' ), 100 ); // Using a late hook.
// Hook to add data attribute to embedded heading via JavaScript.
add_action( 'wp_print_footer_scripts', array( $this, 'inject_heading_data_attributes' ), 99 );
}
/**
* Filters the Stripe Elements configuration array based on saved settings.
*
* Modifies the Stripe Elements appearance configuration to apply custom styles
* based on the saved settings for the current form.
*
* @since 1.0.0
*
* @param array $config The original Elements configuration.
* @param int $form_id The form ID (optional, for backwards compatibility).
* @return array The modified Elements configuration.
*/
public function modify_elements_config( $config, $form_id = null ) {
// Use the provided form ID if available, otherwise fall back to the last rendered form ID.
if ( null === $form_id ) {
// Try to get the relevant form ID from the rendered list (backwards compatibility).
if ( empty( self::$rendered_form_ids ) ) {
return $config;
}
$form_id = end( self::$rendered_form_ids );
if ( ! $form_id ) {
return $config;
}
}
// Check if styling is applicable for this form type.
$display_type = get_post_meta( $form_id, '_form_display_type', true );
if ( ! in_array( $display_type, array( 'embedded', 'overlay' ), true ) ) {
return $config;
}
// --- Apply styles ---
// Ensure appearance key exists.
if ( ! isset( $config['appearance'] ) ) {
$config['appearance'] = array();
}
if ( ! isset( $config['appearance']['variables'] ) ) {
$config['appearance']['variables'] = array();
}
if ( ! isset( $config['appearance']['rules'] ) ) {
$config['appearance']['rules'] = array();
}
// Apply saved styles (Logic remains the same as before).
$primary_color = Settings::get_setting( $form_id, 'primary_color' );
if ( $primary_color ) {
$config['appearance']['variables']['colorPrimary'] = $primary_color;
$config['appearance']['rules']['.Tab:focus']['boxShadow'] = 'inset 0 -4px ' . $primary_color;
$config['appearance']['rules']['.Tab:hover']['boxShadow'] = 'inset 0 -4px ' . $primary_color;
$config['appearance']['rules']['.Tab--selected']['boxShadow'] = 'inset 0 -2px ' . $primary_color;
$config['appearance']['rules']['.Tab--selected:focus']['boxShadow'] = 'inset 0 -4px ' . $primary_color;
$config['appearance']['rules']['.Input:focus']['boxShadow'] = sprintf(
'0 0 0 1px %1$s, 0 0 0 3px %2$s, 0 1px 2px rgba(0, 0, 0, 0.05)',
$primary_color,
self::hex_to_rgba( $primary_color, 0.15 )
);
$config['appearance']['rules']['.CodeInput:focus']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
$config['appearance']['rules']['.CheckboxInput:focus']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
$config['appearance']['rules']['.PickerItem--selected']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
// Add focus styling for dropdown selects.
$config['appearance']['rules']['.p-Select-select:focus']['boxShadow'] = $config['appearance']['rules']['.Input:focus']['boxShadow'];
$config['appearance']['rules']['.p-Select-select:focus']['borderColor'] = $primary_color;
}
$background_color = Settings::get_setting( $form_id, 'background_color' );
if ( $background_color ) {
// Don't set colorBackground variable as it affects all elements including labels.
// Only apply background color to text input fields (text, email, number, etc.), not selects or checkboxes.
// Add explicit background color for text input types only.
$config['appearance']['rules']['.Input']['backgroundColor'] = $background_color;
$config['appearance']['rules']['.CodeInput']['backgroundColor'] = $background_color;
// Explicitly ensure labels, selects, checkboxes, and other non-text inputs don't get the input background color.
$config['appearance']['rules']['.Label']['backgroundColor'] = 'transparent';
$config['appearance']['rules']['.TabLabel']['backgroundColor'] = 'transparent';
$config['appearance']['rules']['.CheckboxInput']['backgroundColor'] = 'transparent';
$config['appearance']['rules']['.p-Select-select']['backgroundColor'] = 'transparent';
$config['appearance']['rules']['.p-Select-option']['backgroundColor'] = 'transparent';
$config['appearance']['rules']['.p-FauxInput']['backgroundColor'] = 'transparent';
}
$text_color = Settings::get_setting( $form_id, 'text_color' );
if ( $text_color ) {
$config['appearance']['variables']['colorText'] = $text_color;
$config['appearance']['rules']['.TabLabel']['color'] = $text_color;
$config['appearance']['rules']['.Label']['color'] = $text_color;
$config['appearance']['rules']['.Input']['color'] = $text_color;
$config['appearance']['rules']['.CodeInput']['color'] = $text_color;
$config['appearance']['rules']['.PickerItem']['color'] = $text_color;
$config['appearance']['rules']['.DropdownItem']['color'] = $text_color;
$config['appearance']['rules']['.TabIcon--selected']['fill'] = $text_color;
// Add text color for select dropdown and options.
$config['appearance']['rules']['.p-Select-select']['color'] = $text_color;
$config['appearance']['rules']['.p-Select-option']['color'] = $text_color;
$config['appearance']['rules']['option']['color'] = $text_color;
}
// Apply label text color (overrides general text color for labels).
$label_text_color = Settings::get_setting( $form_id, 'label_text_color' );
if ( $label_text_color ) {
$config['appearance']['rules']['.Label']['color'] = $label_text_color;
$config['appearance']['rules']['.TabLabel']['color'] = $label_text_color;
}
// Apply input text color (overrides general text color for inputs).
$input_text_color = Settings::get_setting( $form_id, 'input_text_color' );
if ( $input_text_color ) {
$config['appearance']['rules']['.Input']['color'] = $input_text_color;
$config['appearance']['rules']['.CodeInput']['color'] = $input_text_color;
$config['appearance']['rules']['.PickerItem']['color'] = $input_text_color;
$config['appearance']['rules']['.DropdownItem']['color'] = $input_text_color;
$config['appearance']['rules']['.p-Select-select']['color'] = $input_text_color;
$config['appearance']['rules']['.p-Select-option']['color'] = $input_text_color;
$config['appearance']['rules']['option']['color'] = $input_text_color;
$config['appearance']['rules']['.p-FauxInput']['color'] = $input_text_color;
}
// Apply border color.
$border_color = Settings::get_setting( $form_id, 'border_color' );
if ( $border_color ) {
// Add border color to inputs.
$config['appearance']['rules']['.Input']['boxShadow'] = '0 0 0 1px ' . $border_color . ', 0 1px 2px rgba(0, 0, 0, 0.05)';
$config['appearance']['rules']['.CodeInput']['boxShadow'] = $config['appearance']['rules']['.Input']['boxShadow'];
$config['appearance']['rules']['.CheckboxInput']['boxShadow'] = $config['appearance']['rules']['.Input']['boxShadow'];
$config['appearance']['rules']['.p-Select-select']['boxShadow'] = $config['appearance']['rules']['.Input']['boxShadow'];
$config['appearance']['rules']['.p-Select-select']['borderColor'] = $border_color;
$config['appearance']['rules']['.Input']['borderColor'] = $border_color;
}
// Apply error border color.
$error_border_color = Settings::get_setting( $form_id, 'error_border_color' );
if ( $error_border_color ) {
$config['appearance']['rules']['.Input--invalid']['boxShadow'] = '0 0 0 1px ' . $error_border_color . ', 0 1px 2px rgba(0, 0, 0, 0.05)';
$config['appearance']['rules']['.Input--invalid']['borderColor'] = $error_border_color;
}
// Apply error text color.
$error_text_color = Settings::get_setting( $form_id, 'error_text_color' );
if ( $error_text_color ) {
$config['appearance']['rules']['.Error']['color'] = $error_text_color;
$config['appearance']['rules']['.ErrorMessage']['color'] = $error_text_color;
}
$border_radius = Settings::get_setting( $form_id, 'border_radius', '' );
// Only apply border radius if it has been explicitly set.
if ( '' !== $border_radius && Settings::setting_exists( $form_id, 'border_radius' ) ) {
$config['appearance']['variables']['borderRadius'] = $border_radius . 'px';
// Add explicit border radius for select elements.
$config['appearance']['rules']['.p-Select-select']['borderRadius'] = $border_radius . 'px';
}
$input_font_size = Settings::get_setting( $form_id, 'input_font_size' );
if ( $input_font_size ) {
$config['appearance']['rules']['.Input']['fontSize'] = $input_font_size . 'px';
$config['appearance']['rules']['.CodeInput']['fontSize'] = $input_font_size . 'px';
$config['appearance']['rules']['.PickerItem']['fontSize'] = $input_font_size . 'px';
// Add font size for select elements.
$config['appearance']['rules']['.p-Select-select']['fontSize'] = $input_font_size . 'px';
$config['appearance']['rules']['.p-Select-option']['fontSize'] = $input_font_size . 'px';
$config['appearance']['rules']['option']['fontSize'] = $input_font_size . 'px';
}
$label_font_size = Settings::get_setting( $form_id, 'label_font_size' );
if ( $label_font_size ) {
$config['appearance']['rules']['.Label']['fontSize'] = $label_font_size . 'px';
$config['appearance']['rules']['.TabLabel']['fontSize'] = $label_font_size . 'px';
}
$label_font_weight = Settings::get_setting( $form_id, 'label_font_weight' );
if ( $label_font_weight ) {
$config['appearance']['rules']['.Label']['fontWeight'] = $label_font_weight;
$config['appearance']['rules']['.TabLabel']['fontWeight'] = $label_font_weight;
}
// --- End Apply styles. ---
// Remove empty appearance sub-keys so they don't serialize as JSON
// arrays ([]) instead of objects ({}), and so the JS auto-detection
// fallback in elements.js can run when no custom styles are configured.
if ( empty( $config['appearance']['variables'] ) ) {
unset( $config['appearance']['variables'] );
}
if ( empty( $config['appearance']['rules'] ) ) {
unset( $config['appearance']['rules'] );
}
if ( empty( $config['appearance'] ) ) {
unset( $config['appearance'] );
}
return $config;
}
/**
* Capture the ID of a form being rendered on the page.
*
* Stores the form ID in a static array to be used later for styling.
*
* @since 1.0.0
*
* @param int $form_id The ID of the form being rendered.
* @return void
*/
public function capture_rendered_form_id( $form_id ) {
$form_id = absint( $form_id );
if ( $form_id > 0 && ! in_array( $form_id, self::$rendered_form_ids, true ) ) {
self::$rendered_form_ids[] = $form_id;
}
}
/**
* Prints inline CSS late in the footer for non-Elements form parts.
*
* Generates and outputs custom CSS for all rendered forms on the current page.
* This CSS targets non-Stripe Elements parts of the form.
*
* @since 1.0.0
*
* @return void
*/
public function print_late_frontend_styles() {
if ( empty( self::$rendered_form_ids ) ) {
return;
}
$css = '';
$preview_css = ''; // Separate CSS for preview wrapper.
foreach ( self::$rendered_form_ids as $form_id ) {
$display_type = get_post_meta( $form_id, '_form_display_type', true );
if ( ! in_array( $display_type, array( 'embedded', 'overlay' ), true ) ) {
continue;
}
// Get the base CSS from our generator method.
$form_css = $this->generate_custom_css( $form_id );
$css .= $form_css;
// Add legacy CSS selectors for backwards compatibility.
$live_wrapper_selector = "#simpay-embedded-form-wrap-{$form_id}"; // Target the outer wrapper div by ID.
// --- Form Container Background Color & Padding
$form_bg_color = Settings::get_setting( $form_id, 'form_container_background_color' );
if ( $form_bg_color ) {
// Apply background to the specific live wrapper ID (no padding).
$css .= "{$live_wrapper_selector} {
background-color: " . esc_attr( $form_bg_color ) . " !important;
margin: 0 auto !important;
}\n";
// Apply background only to preview wrapper.
$preview_css .= 'body.simpay-form-preview .simpay-form-preview-wrap { background: ' . esc_attr( $form_bg_color ) . " !important; }\n";
}
// --- Form Container Border Radius for preview wrapper.
$form_border_radius = Settings::get_setting( $form_id, 'form_border_radius', '' );
if ( '' !== $form_border_radius && Settings::setting_exists( $form_id, 'form_border_radius' ) ) {
$preview_css .= 'body.simpay-form-preview .simpay-form-preview-wrap { border-radius: ' . absint( $form_border_radius ) . "px !important; }\n";
}
}
// Combine live CSS and preview CSS.
$final_css = $preview_css . $css;
// Output the combined CSS.
if ( ! empty( $final_css ) ) {
// Directly print the CSS in the footer.
echo "\n\n";
}
}
/**
* Get the custom CSS for a specific form.
*
* Public accessor for generate_custom_css(), used by the Gutenberg block
* preview where wp_print_footer_scripts does not fire.
*
* @since 4.17.0
*
* @param int $form_id The form ID.
* @return string The generated CSS, or empty string if not applicable.
*/
public function get_form_css( $form_id ) {
$display_type = get_post_meta( $form_id, '_form_display_type', true );
if ( ! in_array( $display_type, array( 'embedded', 'overlay' ), true ) ) {
return '';
}
return $this->generate_custom_css( $form_id );
}
/**
* Helper function to convert hex color to rgba.
*
* Converts a hexadecimal color value to an rgba color value with the specified opacity.
*
* @since 1.0.0
* @access private
* @static
*
* @param string $color The hexadecimal color value.
* @param float $opacity The opacity value (0-1).
* @return string The rgba color value.
*/
private static function hex_to_rgba( $color, $opacity = 1 ) {
// If the color is already in rgba/rgb format, return it directly.
if ( preg_match( '/^rgba?\(/', $color ) ) {
return $color;
}
$color = ltrim( $color, '#' );
if ( strlen( $color ) === 3 ) {
$r = hexdec( substr( $color, 0, 1 ) . substr( $color, 0, 1 ) );
$g = hexdec( substr( $color, 1, 1 ) . substr( $color, 1, 1 ) );
$b = hexdec( substr( $color, 2, 1 ) . substr( $color, 2, 1 ) );
} elseif ( strlen( $color ) === 6 ) {
$r = hexdec( substr( $color, 0, 2 ) );
$g = hexdec( substr( $color, 2, 2 ) );
$b = hexdec( substr( $color, 4, 2 ) );
} else {
return 'rgba(0,0,0,0)'; // Invalid color.
}
$opacity = max( 0, min( 1, $opacity ) );
return sprintf( 'rgba(%d,%d,%d,%.2f)', $r, $g, $b, $opacity );
}
/**
* Build the box-shadow CSS value used to render a border on form inputs.
*
* Native HTML inputs and Stripe Elements both use a `box-shadow` outline
* to paint borders (the base CSS sets `border: 0`). Centralizing the
* shadow tuple here keeps the rendering consistent and prevents drift
* between the regular border, focus, and error states.
*
* @since 4.17.3
* @access private
* @static
*
* @param string $color The (already-sanitized) color value to use for the inner ring.
* @return string The CSS `box-shadow` value (without the `box-shadow:` property name or trailing `!important`).
*/
private static function build_box_shadow_border( $color ) {
return '0 0 0 1px ' . $color . ', 0 1px 2px rgba(0, 0, 0, 0.05)';
}
/**
* Sanitize a color value for safe inclusion in generated CSS.
*
* Defense-in-depth against legacy or imported postmeta that may have
* bypassed save-time sanitization in {@see Settings::sanitize_setting()}.
* Accepts hex colors and `rgb()`/`rgba()`/`hsl()`/`hsla()` notation.
* Anything else (including `javascript:`, escape sequences, or arbitrary
* strings that could break out of the CSS context) is rejected.
*
* @since 4.17.3
* @access private
* @static
*
* @param string $color The raw color value from settings.
* @return string A safe color value, or empty string if the input is unsafe.
*/
private static function sanitize_color_for_css( $color ) {
if ( ! is_string( $color ) || '' === $color ) {
return '';
}
// Hex color (#fff, #ffffff). sanitize_hex_color returns null/empty on bad input.
$hex = sanitize_hex_color( $color );
if ( ! empty( $hex ) ) {
return $hex;
}
// Functional notation: rgb(), rgba(), hsl(), hsla(). Match the same
// shape Settings::sanitize_setting() accepts.
if ( preg_match( '/^(rgba?|hsla?)\(\s*[\d.,\s%]+\)$/', $color ) ) {
return $color;
}
// Anything else is rejected to prevent CSS injection.
return '';
}
/**
* Generate the custom CSS for a form based on its style settings.
*
* Creates CSS rules for a specific form based on its saved style settings.
* This CSS targets standard HTML elements that are not controlled by Stripe Elements.
*
* @since 1.0.0
* @access private
*
* @param int $form_id The form ID.
* @return string The generated CSS.
*/
private function generate_custom_css( $form_id ) {
$css = '';
// Only add container width to make sure the form doesn't get squeezed.
$css .= "
/* Form container width */
#simpay-embedded-form-wrap-{$form_id} {
max-width: none !important;
}";
// Now add the theme-specific styles
// Add form container background color.
$form_bg_color = Settings::get_setting( $form_id, 'form_container_background_color' );
if ( ! empty( $form_bg_color ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
$css .= "#simpay-form-{$form_id} { background-color: {$form_bg_color} !important; }\n";
$css .= "#simpay-embedded-form-wrap-{$form_id} { background-color: {$form_bg_color} !important; }\n";
$css .= ".simpay-checkout-form--embedded[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
// Apply to overlay modal content.
$css .= ".simpay-modal__content[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
$css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content { background-color: {$form_bg_color} !important; }\n";
$css .= ".simpay-checkout-form--overlay[data-form-id=\"{$form_id}\"] { background-color: {$form_bg_color} !important; }\n";
}
// Add form padding.
$form_padding = Settings::get_setting( $form_id, 'form_padding', '' );
if ( '' !== $form_padding && Settings::setting_exists( $form_id, 'form_padding' ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
$css .= "#simpay-form-{$form_id} { padding: {$form_padding}px !important; }\n";
$css .= "#simpay-embedded-form-wrap-{$form_id} { padding: {$form_padding}px !important; }\n";
$css .= ".simpay-checkout-form--embedded[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
// Apply to overlay modal content.
$css .= ".simpay-modal__content[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
$css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content { padding: {$form_padding}px !important; }\n";
$css .= ".simpay-checkout-form--overlay[data-form-id=\"{$form_id}\"] { padding: {$form_padding}px !important; }\n";
}
// Add form background color (input fields - text inputs only).
$bg_color = Settings::get_setting( $form_id, 'background_color' );
if ( ! empty( $bg_color ) ) {
// Target only text input types (text, email, number, tel, password), NOT selects, checkboxes, or textareas.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='text'],\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='email'],\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='tel'],\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='number'],\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='password'],\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='url'],\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='search'] { background-color: {$bg_color} !important; }\n";
$css .= "#simpay-form-{$form_id} input[type='text'],\n";
$css .= "#simpay-form-{$form_id} input[type='email'],\n";
$css .= "#simpay-form-{$form_id} input[type='tel'],\n";
$css .= "#simpay-form-{$form_id} input[type='number'],\n";
$css .= "#simpay-form-{$form_id} input[type='password'],\n";
$css .= "#simpay-form-{$form_id} input[type='url'],\n";
$css .= "#simpay-form-{$form_id} input[type='search'] { background-color: {$bg_color} !important; }\n";
// Target Stripe Elements text inputs only (not selects or checkboxes).
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { background-color: {$bg_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .CodeInput { background-color: {$bg_color} !important; }\n";
// Explicitly ensure selects, checkboxes, and other non-text inputs don't get the background color.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] select { background-color: transparent !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] textarea { background-color: transparent !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='checkbox'] { background-color: transparent !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input[type='radio'] { background-color: transparent !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { background-color: transparent !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .CheckboxInput { background-color: transparent !important; }\n";
$css .= "#simpay-form-{$form_id} select { background-color: transparent !important; }\n";
$css .= "#simpay-form-{$form_id} textarea { background-color: transparent !important; }\n";
$css .= "#simpay-form-{$form_id} input[type='checkbox'] { background-color: transparent !important; }\n";
$css .= "#simpay-form-{$form_id} input[type='radio'] { background-color: transparent !important; }\n";
}
// Add text color (general text color - applies to all text elements).
$text_color = Settings::get_setting( $form_id, 'text_color' );
if ( ! empty( $text_color ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] * { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control { color: {$text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} { color: {$text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} * { color: {$text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-form-control { color: {$text_color} !important; }\n";
// Target Stripe Elements text colors - including dropdowns, inputs and labels.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Label { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement input { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement option { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement p { color: {$text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement span { color: {$text_color} !important; }\n";
}
// Add label text color (overrides text color for labels).
$label_text_color = Settings::get_setting( $form_id, 'label_text_color' );
if ( ! empty( $label_text_color ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] label { color: {$label_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-label { color: {$label_text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} label { color: {$label_text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-label { color: {$label_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Label { color: {$label_text_color} !important; }\n";
}
// Add input text color (overrides text color for inputs).
$input_text_color = Settings::get_setting( $form_id, 'input_text_color' );
if ( ! empty( $input_text_color ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] select { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] textarea { color: {$input_text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} input { color: {$input_text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} select { color: {$input_text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} textarea { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement input { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement option { color: {$input_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-FauxInput { color: {$input_text_color} !important; }\n";
}
// Add border color.
$border_color = Settings::get_setting( $form_id, 'border_color' );
if ( ! empty( $border_color ) ) {
// Re-sanitize at output time as defense-in-depth against
// legacy/imported postmeta that may have bypassed save-time sanitization.
$border_color = self::sanitize_color_for_css( $border_color );
}
if ( ! empty( $border_color ) ) {
// Native HTML inputs use box-shadow for borders (base CSS sets border: 0).
$box_shadow_border = 'box-shadow: ' . self::build_box_shadow_border( $border_color ) . ' !important;';
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control { border-color: {$border_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-form-control { border-color: {$border_color} !important; }\n";
$css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='text'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='email'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='tel'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='number'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='password'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='url'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='search'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control select, #simpay-form-{$form_id}.simpay-styled .simpay-form-control textarea { {$box_shadow_border} }\n";
// Add specific style for Stripe elements.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement { border-color: {$border_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { {$box_shadow_border} border-color: {$border_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .CodeInput { {$box_shadow_border} border-color: {$border_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { border-color: {$border_color} !important; }\n";
}
// Add error border color.
$error_border_color = Settings::get_setting( $form_id, 'error_border_color' );
if ( ! empty( $error_border_color ) ) {
// Re-sanitize at output time as defense-in-depth against
// legacy/imported postmeta that may have bypassed save-time sanitization.
$error_border_color = self::sanitize_color_for_css( $error_border_color );
}
if ( ! empty( $error_border_color ) ) {
$error_box_shadow = 'box-shadow: ' . self::build_box_shadow_border( $error_border_color ) . ' !important;';
// Native inputs use box-shadow for border rendering (matches .simpay-input-error in SCSS).
// Also emit `border-color` so themes that paint real borders pick up the error color.
//
// Note on specificity: the base `border_color` rule above emits
// `box-shadow !important` under a (1,3,1)-specificity selector
// (`#simpay-form-{id}.simpay-styled .simpay-form-control input[type='text'], ...`).
// The lower-specificity error variants below would lose the cascade
// when `border_color` is also set, so we additionally emit a rule
// using the same (1,3,1) chain so the error color reliably wins
// (later rule of equal specificity wins under `!important`).
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input.simpay-input-error { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
$css .= "#simpay-form-{$form_id} input.simpay-input-error { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
$css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input.simpay-input-error { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
// Native HTML5 validation fallback. `:user-invalid` is preferred over
// `:invalid` because it only matches after user interaction, avoiding
// red borders on initial render. Scoped to this form to avoid clobbering
// globals or other forms on the page. The third rule matches the base
// border specificity (see note above).
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] input:user-invalid, .simpay-form-wrap[data-form-id=\"{$form_id}\"] select:user-invalid, .simpay-form-wrap[data-form-id=\"{$form_id}\"] textarea:user-invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
$css .= "#simpay-form-{$form_id} input:user-invalid, #simpay-form-{$form_id} select:user-invalid, #simpay-form-{$form_id} textarea:user-invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
$css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input:user-invalid, #simpay-form-{$form_id}.simpay-styled .simpay-form-control select:user-invalid, #simpay-form-{$form_id}.simpay-styled .simpay-form-control textarea:user-invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
// Stripe Elements invalid state.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input--invalid { {$error_box_shadow} border-color: {$error_border_color} !important; }\n";
}
// Add error text color.
$error_text_color = Settings::get_setting( $form_id, 'error_text_color' );
if ( ! empty( $error_text_color ) ) {
// Re-sanitize at output time as defense-in-depth against
// legacy/imported postmeta that may have bypassed save-time sanitization.
$error_text_color = self::sanitize_color_for_css( $error_text_color );
}
if ( ! empty( $error_text_color ) ) {
// .simpay-errors is the class used on all inline field error message elements.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-errors { color: {$error_text_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-errors { color: {$error_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Error { color: {$error_text_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .ErrorMessage { color: {$error_text_color} !important; }\n";
}
// Add primary color (accent color - used for focus states, links, etc.).
$primary_color = Settings::get_setting( $form_id, 'primary_color' );
if ( ! empty( $primary_color ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control:focus { border-color: {$primary_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] a { color: {$primary_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-form-control:focus { border-color: {$primary_color} !important; }\n";
$css .= "#simpay-form-{$form_id} a { color: {$primary_color} !important; }\n";
// Target Stripe Elements focus states.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input:focus { border-color: {$primary_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input:focus { border-color: {$primary_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select:focus { border-color: {$primary_color} !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input:focus { box-shadow: 0 0 0 1px {$primary_color}, 0 0 0 3px " . self::hex_to_rgba( $primary_color, 0.15 ) . ", 0 1px 2px rgba(0, 0, 0, 0.05) !important; }\n";
}
// Add form container border radius (separate from input/button border radius).
$form_border_radius = Settings::get_setting( $form_id, 'form_border_radius', '' );
if ( '' !== $form_border_radius && Settings::setting_exists( $form_id, 'form_border_radius' ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
$css .= "#simpay-form-{$form_id} { border-radius: {$form_border_radius}px !important; }\n";
$css .= "#simpay-embedded-form-wrap-{$form_id} { border-radius: {$form_border_radius}px !important; }\n";
$css .= ".simpay-checkout-form--embedded[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
// Apply to overlay form modal content.
$css .= ".simpay-modal__content[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
$css .= ".simpay-modal[data-form-id=\"{$form_id}\"] .simpay-modal__content { border-radius: {$form_border_radius}px !important; }\n";
$css .= ".simpay-modal[data-form-id=\"{$form_id}\"] { border-radius: {$form_border_radius}px !important; }\n";
}
// Add border radius for inputs, buttons, and form controls.
$border_radius = Settings::get_setting( $form_id, 'border_radius', '' );
if ( '' !== $border_radius && Settings::setting_exists( $form_id, 'border_radius' ) ) {
// For form controls.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-form-control { border-radius: {$border_radius}px !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-form-control { border-radius: {$border_radius}px !important; }\n";
// Override .simpay-styled .simpay-form-control input/select/textarea border-radius, scoped to this form.
$css .= "#simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='text'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='email'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='tel'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='number'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='password'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='url'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control input[type='search'], #simpay-form-{$form_id}.simpay-styled .simpay-form-control select, #simpay-form-{$form_id}.simpay-styled .simpay-form-control textarea { border-radius: {$border_radius}px !important; }\n";
// Target Stripe Elements border radius.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .Input { border-radius: {$border_radius}px !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement select.Input { border-radius: {$border_radius}px !important; }\n";
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .StripeElement .p-Select-select { border-radius: {$border_radius}px !important; }\n";
// Apply to buttons.
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-payment-btn { border-radius: {$border_radius}px !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-payment-btn { border-radius: {$border_radius}px !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-checkout-btn { border-radius: {$border_radius}px !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-apply-coupon { border-radius: {$border_radius}px !important; }\n";
// Target buttons with higher specificity to override WP Simple Pay defaults.
$css .= "body .simpay-form-wrap[data-form-id=\"{$form_id}\"] button.simpay-payment-btn { border-radius: {$border_radius}px !important; }\n";
$css .= "body #simpay-form-{$form_id} button.simpay-checkout-btn { border-radius: {$border_radius}px !important; }\n";
$css .= "body #simpay-form-{$form_id} button.simpay-apply-coupon { border-radius: {$border_radius}px !important; }\n";
}
// Add button background color.
$button_bg_color = Settings::get_raw_setting( $form_id, 'button_background_color' );
if ( ! empty( $button_bg_color ) ) {
$css .= ".simpay-form-wrap[data-form-id=\"{$form_id}\"] .simpay-payment-btn { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-payment-btn { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-checkout-btn { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
$css .= "#simpay-form-{$form_id} .simpay-apply-coupon { background-color: {$button_bg_color} !important; border-color: {$button_bg_color} !important; }\n";
}
// Add button text color.
// Default to white when text_color is set, since the text_color wildcard
// (* selector) would otherwise override the button's white text from SCSS.
$button_text_color = Settings::get_raw_setting( $form_id, 'button_text_color' );
if ( empty( $button_text_color ) && ! empty( $text_color ) ) {
$button_text_color = '#ffffff';
}
if ( ! empty( $button_text_color ) ) {
// Target the button and all children (e.g. inside