diff --git a/src/WorkOS/Auth/AuthKit/Profile.php b/src/WorkOS/Auth/AuthKit/Profile.php index d7a880a..d7f51b1 100644 --- a/src/WorkOS/Auth/AuthKit/Profile.php +++ b/src/WorkOS/Auth/AuthKit/Profile.php @@ -149,7 +149,7 @@ class Profile { /** * Branding config. * - * @var array{logo_mode: string, logo_attachment_id: int, primary_color: string, heading: string, subheading: string} + * @var array{logo_mode: string, logo_attachment_id: int, card_border_radius: string, button_border_radius: string, page_background: string, card_background: string, card_border: string, heading_color: string, subheading_color: string, button_background: string, button_text: string, secondary_button_background: string, secondary_button_text: string, secondary_button_border: string, links_color: string, heading: string, subheading: string} */ private array $branding; @@ -201,11 +201,23 @@ public function __construct( array $data ) { 'factors' => array_values( array_filter( (array) ( $data['mfa']['factors'] ?? [] ), 'is_string' ) ), ]; $this->branding = [ - 'logo_mode' => (string) ( $data['branding']['logo_mode'] ?? self::LOGO_MODE_DEFAULT ), - 'logo_attachment_id' => (int) ( $data['branding']['logo_attachment_id'] ?? 0 ), - 'primary_color' => (string) ( $data['branding']['primary_color'] ?? '' ), - 'heading' => (string) ( $data['branding']['heading'] ?? '' ), - 'subheading' => (string) ( $data['branding']['subheading'] ?? '' ), + 'logo_mode' => (string) ( $data['branding']['logo_mode'] ?? self::LOGO_MODE_DEFAULT ), + 'logo_attachment_id' => (int) ( $data['branding']['logo_attachment_id'] ?? 0 ), + 'card_border_radius' => (string) ( $data['branding']['card_border_radius'] ?? '' ), + 'button_border_radius' => (string) ( $data['branding']['button_border_radius'] ?? '' ), + 'page_background' => (string) ( $data['branding']['page_background'] ?? '' ), + 'card_background' => (string) ( $data['branding']['card_background'] ?? '' ), + 'card_border' => (string) ( $data['branding']['card_border'] ?? '' ), + 'heading_color' => (string) ( $data['branding']['heading_color'] ?? '' ), + 'subheading_color' => (string) ( $data['branding']['subheading_color'] ?? '' ), + 'button_background' => (string) ( $data['branding']['button_background'] ?? '' ), + 'button_text' => (string) ( $data['branding']['button_text'] ?? '' ), + 'secondary_button_background' => (string) ( $data['branding']['secondary_button_background'] ?? '' ), + 'secondary_button_text' => (string) ( $data['branding']['secondary_button_text'] ?? '' ), + 'secondary_button_border' => (string) ( $data['branding']['secondary_button_border'] ?? '' ), + 'links_color' => (string) ( $data['branding']['links_color'] ?? '' ), + 'heading' => (string) ( $data['branding']['heading'] ?? '' ), + 'subheading' => (string) ( $data['branding']['subheading'] ?? '' ), ]; $this->post_login_redirect = (string) ( $data['post_login_redirect'] ?? '' ); $this->forward_query_args = (bool) ( $data['forward_query_args'] ?? false ); @@ -297,10 +309,27 @@ public static function from_array( array $data ): self { $organization_id = ''; } - $primary_color = (string) ( $data['branding']['primary_color'] ?? '' ); - if ( '' !== $primary_color && ! preg_match( '/^#[0-9a-fA-F]{3,8}$/', $primary_color ) ) { - $primary_color = ''; - } + $sanitize_color = static function ( string $value ): string { + return ( '' !== $value && preg_match( '/^#[0-9a-fA-F]{3,8}$/', $value ) ) ? $value : ''; + }; + + $sanitize_radius = static function ( string $v ): string { + $v = trim( $v ); + return ( '' !== $v && ctype_digit( $v ) ) ? $v : ''; + }; + $card_border_radius = $sanitize_radius( (string) ( $data['branding']['card_border_radius'] ?? '' ) ); + $button_border_radius = $sanitize_radius( (string) ( $data['branding']['button_border_radius'] ?? '' ) ); + $page_background = $sanitize_color( (string) ( $data['branding']['page_background'] ?? '' ) ); + $card_background = $sanitize_color( (string) ( $data['branding']['card_background'] ?? '' ) ); + $card_border = $sanitize_color( (string) ( $data['branding']['card_border'] ?? '' ) ); + $heading_color = $sanitize_color( (string) ( $data['branding']['heading_color'] ?? '' ) ); + $subheading_color = $sanitize_color( (string) ( $data['branding']['subheading_color'] ?? '' ) ); + $button_background = $sanitize_color( (string) ( $data['branding']['button_background'] ?? '' ) ); + $button_text = $sanitize_color( (string) ( $data['branding']['button_text'] ?? '' ) ); + $secondary_button_background = $sanitize_color( (string) ( $data['branding']['secondary_button_background'] ?? '' ) ); + $secondary_button_text = $sanitize_color( (string) ( $data['branding']['secondary_button_text'] ?? '' ) ); + $secondary_button_border = $sanitize_color( (string) ( $data['branding']['secondary_button_border'] ?? '' ) ); + $links_color = $sanitize_color( (string) ( $data['branding']['links_color'] ?? '' ) ); $logo_attachment_id = (int) ( $data['branding']['logo_attachment_id'] ?? 0 ); @@ -341,11 +370,23 @@ public static function from_array( array $data ): self { 'factors' => $mfa_factors, ], 'branding' => [ - 'logo_mode' => $logo_mode, - 'logo_attachment_id' => $logo_attachment_id, - 'primary_color' => $primary_color, - 'heading' => sanitize_text_field( (string) ( $data['branding']['heading'] ?? '' ) ), - 'subheading' => sanitize_text_field( (string) ( $data['branding']['subheading'] ?? '' ) ), + 'logo_mode' => $logo_mode, + 'logo_attachment_id' => $logo_attachment_id, + 'card_border_radius' => $card_border_radius, + 'button_border_radius' => $button_border_radius, + 'page_background' => $page_background, + 'card_background' => $card_background, + 'card_border' => $card_border, + 'heading_color' => $heading_color, + 'subheading_color' => $subheading_color, + 'button_background' => $button_background, + 'button_text' => $button_text, + 'secondary_button_background' => $secondary_button_background, + 'secondary_button_text' => $secondary_button_text, + 'secondary_button_border' => $secondary_button_border, + 'links_color' => $links_color, + 'heading' => sanitize_text_field( (string) ( $data['branding']['heading'] ?? '' ) ), + 'subheading' => sanitize_text_field( (string) ( $data['branding']['subheading'] ?? '' ) ), ], 'post_login_redirect' => sanitize_text_field( (string) ( $data['post_login_redirect'] ?? '' ) ), 'forward_query_args' => (bool) ( $data['forward_query_args'] ?? false ), @@ -384,7 +425,12 @@ public static function defaults(): self { 'branding' => [ 'logo_mode' => self::LOGO_MODE_DEFAULT, 'logo_attachment_id' => 0, - 'primary_color' => '', + 'page_background' => '', + 'card_background' => '', + 'card_border' => '', + 'button_background' => '', + 'button_text' => '', + 'links_color' => '', 'heading' => __( 'Sign in', 'integration-workos' ), 'subheading' => '', ], @@ -620,7 +666,7 @@ public function get_mfa(): array { /** * Branding configuration. * - * @return array{logo_attachment_id: int, primary_color: string, heading: string, subheading: string} + * @return array{logo_attachment_id: int, page_background: string, card_background: string, card_border: string, button_background: string, button_text: string, links_color: string, heading: string, subheading: string} */ public function get_branding(): array { return $this->branding; diff --git a/src/WorkOS/Auth/AuthKit/Renderer.php b/src/WorkOS/Auth/AuthKit/Renderer.php index 3c3c4f4..592c733 100644 --- a/src/WorkOS/Auth/AuthKit/Renderer.php +++ b/src/WorkOS/Auth/AuthKit/Renderer.php @@ -291,10 +291,22 @@ private function resolve_branding( Profile $profile ): array { } $resolved = [ - 'logo_url' => $logo_url, - 'primary_color' => (string) ( $branding['primary_color'] ?? '' ), - 'heading' => (string) ( $branding['heading'] ?? '' ), - 'subheading' => (string) ( $branding['subheading'] ?? '' ), + 'logo_url' => $logo_url, + 'card_border_radius' => (string) ( $branding['card_border_radius'] ?? '' ), + 'button_border_radius' => (string) ( $branding['button_border_radius'] ?? '' ), + 'page_background' => (string) ( $branding['page_background'] ?? '' ), + 'card_background' => (string) ( $branding['card_background'] ?? '' ), + 'card_border' => (string) ( $branding['card_border'] ?? '' ), + 'heading_color' => (string) ( $branding['heading_color'] ?? '' ), + 'subheading_color' => (string) ( $branding['subheading_color'] ?? '' ), + 'button_background' => (string) ( $branding['button_background'] ?? '' ), + 'button_text' => (string) ( $branding['button_text'] ?? '' ), + 'secondary_button_background' => (string) ( $branding['secondary_button_background'] ?? '' ), + 'secondary_button_text' => (string) ( $branding['secondary_button_text'] ?? '' ), + 'secondary_button_border' => (string) ( $branding['secondary_button_border'] ?? '' ), + 'links_color' => (string) ( $branding['links_color'] ?? '' ), + 'heading' => (string) ( $branding['heading'] ?? '' ), + 'subheading' => (string) ( $branding['subheading'] ?? '' ), ]; /** @@ -323,28 +335,95 @@ private function resolve_branding( Profile $profile ): array { private function branding_style_tag( array $branding ): string { $rules = []; - // Re-validate the primary color as defense-in-depth. Profile::from_array - // already regex-matches it against a hex pattern on save, but this - // renderer is the last line before raw emission into a CSS context - // where `esc_attr` is semantically wrong. Enforcing the same regex - // here guarantees that whatever arrives in $branding — now or from - // future call sites — cannot introduce a semicolon, closing brace, - // or `` sequence. - $primary = (string) ( $branding['primary_color'] ?? '' ); - if ( '' !== $primary && preg_match( '/^#[0-9a-fA-F]{3,8}$/', $primary ) ) { - // A custom primary drops the matching WP-blue hover, so override - // hover to the same color. Deriving a darker shade would require - // a color-math utility and is not worth the added surface area; - // a flat hover reads cleanly enough for a branded palette. - $rules[] = '--wa-primary: ' . $primary . ';'; - $rules[] = '--wa-primary-hover: ' . $primary . ';'; + // Re-validate colors as defense-in-depth. Profile::from_array already + // regex-matches them on save, but this is the last line before raw + // emission into a CSS context. Enforcing the same regex here ensures + // no semicolon, closing brace, or can be injected regardless + // of call site. + $valid_hex = static function ( string $v ): bool { + return '' !== $v && (bool) preg_match( '/^#[0-9a-fA-F]{3,8}$/', $v ); + }; + + $card_radius = (string) ( $branding['card_border_radius'] ?? '' ); + $btn_radius = (string) ( $branding['button_border_radius'] ?? '' ); + $page_bg = (string) ( $branding['page_background'] ?? '' ); + $card_bg = (string) ( $branding['card_background'] ?? '' ); + $card_bdr = (string) ( $branding['card_border'] ?? '' ); + $heading_clr = (string) ( $branding['heading_color'] ?? '' ); + $sub_clr = (string) ( $branding['subheading_color'] ?? '' ); + $btn_bg = (string) ( $branding['button_background'] ?? '' ); + $btn_text = (string) ( $branding['button_text'] ?? '' ); + $sec_btn_bg = (string) ( $branding['secondary_button_background'] ?? '' ); + $sec_btn_text = (string) ( $branding['secondary_button_text'] ?? '' ); + $sec_btn_bdr = (string) ( $branding['secondary_button_border'] ?? '' ); + $links = (string) ( $branding['links_color'] ?? '' ); + + if ( '' !== $card_radius && ctype_digit( $card_radius ) ) { + $rules[] = '--wa-card-radius:' . (int) $card_radius . 'px;'; } - if ( empty( $rules ) ) { - return ''; + if ( '' !== $btn_radius && ctype_digit( $btn_radius ) ) { + $rules[] = '--wa-btn-radius:' . (int) $btn_radius . 'px;'; } - return ''; + // Page background targets the takeover body, not the root widget. + $body_rules = []; + if ( $valid_hex( $page_bg ) ) { + $body_rules[] = 'background:' . $page_bg . ';'; + } + + if ( $valid_hex( $card_bg ) ) { + $rules[] = '--wa-surface:' . $card_bg . ';'; + } + + if ( $valid_hex( $card_bdr ) ) { + $rules[] = '--wa-card-border:' . $card_bdr . ';'; + } + + if ( $valid_hex( $heading_clr ) ) { + $rules[] = '--wa-heading-color:' . $heading_clr . ';'; + } + + if ( $valid_hex( $sub_clr ) ) { + $rules[] = '--wa-subheading-color:' . $sub_clr . ';'; + } + + if ( $valid_hex( $btn_bg ) ) { + // A custom button color drops the matching WP-blue hover; use the + // same value for hover (flat) rather than deriving a darker shade. + $rules[] = '--wa-primary:' . $btn_bg . ';'; + $rules[] = '--wa-primary-hover:' . $btn_bg . ';'; + } + + if ( $valid_hex( $btn_text ) ) { + $rules[] = '--wa-primary-fg:' . $btn_text . ';'; + } + + if ( $valid_hex( $sec_btn_bg ) ) { + $rules[] = '--wa-secondary-bg:' . $sec_btn_bg . ';'; + } + + if ( $valid_hex( $sec_btn_text ) ) { + $rules[] = '--wa-secondary-fg:' . $sec_btn_text . ';'; + } + + if ( $valid_hex( $sec_btn_bdr ) ) { + $rules[] = '--wa-secondary-border:' . $sec_btn_bdr . ';'; + } + + if ( $valid_hex( $links ) ) { + $rules[] = '--wa-links:' . $links . ';'; + } + + $out = ''; + if ( ! empty( $body_rules ) ) { + $out .= ''; + } + if ( ! empty( $rules ) ) { + $out .= ''; + } + + return $out; } /** diff --git a/src/js/admin-profiles/index.tsx b/src/js/admin-profiles/index.tsx index 19b39f1..fb465f5 100644 --- a/src/js/admin-profiles/index.tsx +++ b/src/js/admin-profiles/index.tsx @@ -103,7 +103,19 @@ interface Profile { logo_mode: LogoMode; logo_attachment_id: number; logo_url?: string; - primary_color: string; + card_border_radius: string; + button_border_radius: string; + page_background: string; + card_background: string; + card_border: string; + heading_color: string; + subheading_color: string; + button_background: string; + button_text: string; + secondary_button_background: string; + secondary_button_text: string; + secondary_button_border: string; + links_color: string; heading: string; subheading: string; }; @@ -684,6 +696,48 @@ function LogoField( { mode, attachmentId, url, onChange }: LogoFieldProps ) { ); } +interface ColorRowProps { + label: string; + value: string; + onChange: ( next: string ) => void; +} + +function ColorRow( { label, value, onChange }: ColorRowProps ) { + const safeColor = /^#[0-9a-fA-F]{6}$/.test( value ) ? value : '#000000'; + return ( +