*/ 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