/) and booking pages (/book/). define( 'FDCHAT_PUBLIC_BASE', 'https://frontdeskchat.com' ); // Same defaults the widget itself uses when an attribute is missing. define( 'FDCHAT_DEFAULT_COLOR', '#2563eb' ); define( 'FDCHAT_DEFAULT_POSITION', 'right' ); // Booking button and site-wide floating element: off until the owner opts in. define( 'FDCHAT_DEFAULT_TEXT', 'Book a call' ); define( 'FDCHAT_DEFAULT_TEXT_COLOR', '#ffffff' ); define( 'FDCHAT_DEFAULT_FLOAT', 'off' ); define( 'FDCHAT_DEFAULT_DISPLAY', 'inline' ); /* ------------------------------------------------------------------------ * Validation. Every value is validated on save and again when it is used. * --------------------------------------------------------------------- */ /** * Desk ids are UUIDs. Returns the normalised id, or '' when invalid. */ function fdchat_valid_desk_id( $value ) { $value = strtolower( trim( (string) $value ) ); return preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/', $value ) ? $value : ''; } function fdchat_sanitize_desk_id( $input ) { $input = trim( (string) $input ); if ( '' === $input ) { return ''; } $desk_id = fdchat_valid_desk_id( $input ); if ( '' === $desk_id ) { add_settings_error( 'fdchat_desk_id', 'fdchat_desk_id_invalid', __( 'The desk id must look like 123e4567-e89b-12d3-a456-426614174000. Copy it from your FrontDesk dashboard.', 'frontdesk-chat' ) ); return get_option( 'fdchat_desk_id', '' ); } return $desk_id; } function fdchat_sanitize_color( $value, $default = FDCHAT_DEFAULT_COLOR ) { $color = sanitize_hex_color( (string) $value ); return $color ? $color : $default; } function fdchat_sanitize_text_color( $value ) { return fdchat_sanitize_color( $value, FDCHAT_DEFAULT_TEXT_COLOR ); } function fdchat_sanitize_position( $value ) { return in_array( $value, array( 'left', 'right' ), true ) ? $value : FDCHAT_DEFAULT_POSITION; } /** * Site-wide floating element: nothing, the booking button, or the chat bubble. */ function fdchat_sanitize_float( $value ) { return in_array( $value, array( 'off', 'badge', 'chat' ), true ) ? $value : FDCHAT_DEFAULT_FLOAT; } /** * How an inline-placed booking page is shown: on the page, or behind a text link or a button. */ function fdchat_sanitize_display( $value ) { return in_array( $value, array( 'inline', 'text', 'button' ), true ) ? $value : FDCHAT_DEFAULT_DISPLAY; } function fdchat_sanitize_text( $value ) { $text = trim( mb_substr( sanitize_text_field( (string) $value ), 0, 40 ) ); return '' === $text ? FDCHAT_DEFAULT_TEXT : $text; } /** * Inline chat page: only https://frontdeskchat.com// is accepted. * No query, fragment, trailing slash, extra segment or foreign host can reach an iframe src. */ function fdchat_valid_chat_url( $url ) { $url = trim( (string) $url ); $pattern = '#^' . preg_quote( FDCHAT_PUBLIC_BASE, '#' ) . '/[A-Za-z0-9._-]+/[A-Za-z0-9._-]+\z#'; return preg_match( $pattern, $url ) ? $url : ''; } /** * Inline booking page, built from a valid desk id (never from user-supplied URLs). */ function fdchat_booking_url( $desk_id ) { $desk_id = fdchat_valid_desk_id( $desk_id ); return '' === $desk_id ? '' : FDCHAT_PUBLIC_BASE . '/book/' . $desk_id; } /** * Site-wide floating element, re-validated on every use. Null means: no floating element, * either because the owner left it off or because there is no valid desk id. */ function fdchat_get_config() { $float = fdchat_sanitize_float( get_option( 'fdchat_float', FDCHAT_DEFAULT_FLOAT ) ); $desk_id = fdchat_valid_desk_id( get_option( 'fdchat_desk_id', '' ) ); if ( 'off' === $float || '' === $desk_id ) { return null; } return array( 'float' => $float, 'desk' => $desk_id, 'color' => fdchat_sanitize_color( get_option( 'fdchat_color', FDCHAT_DEFAULT_COLOR ) ), 'position' => fdchat_sanitize_position( get_option( 'fdchat_position', FDCHAT_DEFAULT_POSITION ) ), 'text' => fdchat_sanitize_text( get_option( 'fdchat_text', FDCHAT_DEFAULT_TEXT ) ), 'textColor' => fdchat_sanitize_text_color( get_option( 'fdchat_text_color', FDCHAT_DEFAULT_TEXT_COLOR ) ), 'booking' => fdchat_booking_url( $desk_id ), 'src' => FDCHAT_WIDGET_SRC, 'app' => FDCHAT_APP_ORIGIN, ); } /* ------------------------------------------------------------------------ * Settings * --------------------------------------------------------------------- */ function fdchat_register_settings() { register_setting( 'fdchat_settings', 'fdchat_desk_id', array( 'type' => 'string', 'sanitize_callback' => 'fdchat_sanitize_desk_id', 'default' => '' ) ); register_setting( 'fdchat_settings', 'fdchat_float', array( 'type' => 'string', 'sanitize_callback' => 'fdchat_sanitize_float', 'default' => FDCHAT_DEFAULT_FLOAT ) ); register_setting( 'fdchat_settings', 'fdchat_text', array( 'type' => 'string', 'sanitize_callback' => 'fdchat_sanitize_text', 'default' => FDCHAT_DEFAULT_TEXT ) ); register_setting( 'fdchat_settings', 'fdchat_color', array( 'type' => 'string', 'sanitize_callback' => 'fdchat_sanitize_color', 'default' => FDCHAT_DEFAULT_COLOR ) ); register_setting( 'fdchat_settings', 'fdchat_text_color', array( 'type' => 'string', 'sanitize_callback' => 'fdchat_sanitize_text_color', 'default' => FDCHAT_DEFAULT_TEXT_COLOR ) ); register_setting( 'fdchat_settings', 'fdchat_position', array( 'type' => 'string', 'sanitize_callback' => 'fdchat_sanitize_position', 'default' => FDCHAT_DEFAULT_POSITION ) ); } add_action( 'admin_init', 'fdchat_register_settings' ); function fdchat_add_settings_page() { add_options_page( __( 'FrontDesk Chat', 'frontdesk-chat' ), __( 'FrontDesk Chat', 'frontdesk-chat' ), 'manage_options', 'frontdeskchat', 'fdchat_render_settings_page' ); } add_action( 'admin_menu', 'fdchat_add_settings_page' ); function fdchat_text_row( $name, $label, $help, $placeholder = '' ) { printf( '

%5$s

', esc_attr( $name ), esc_html( $label ), esc_attr( get_option( $name, '' ) ), esc_attr( $placeholder ), esc_html( $help ) ); } function fdchat_select_row( $name, $label, $choices, $current ) { echo ''; } function fdchat_render_consent_help() { echo '

' . esc_html__( 'Visitor consent', 'frontdesk-chat' ) . '

'; echo '

' . esc_html__( 'Nothing from FrontDesk loads until your consent manager tells this plugin the visitor agreed. Add this line where your consent manager runs after a visitor accepts:', 'frontdesk-chat' ) . '

'; echo '
' . esc_html( "window.fdchatConsentGranted = true; document.dispatchEvent( new Event( 'fdchat_consent_granted' ) );" ) . '
'; echo '

' . esc_html__( 'If you use the chat bubble, once it has loaded you can also pass consent categories to it:', 'frontdesk-chat' ) . ' ' . esc_html( 'FrontDeskChat.setConsent( { personalization: true, analytics: true } )' ) . '

'; } function fdchat_render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } echo '

' . esc_html( get_admin_page_title() ) . '

'; echo '
'; settings_fields( 'fdchat_settings' ); echo ''; fdchat_text_row( 'fdchat_desk_id', __( 'Desk id', 'frontdesk-chat' ), __( 'Find it at the end of your booking link (https://frontdeskchat.com/book/), or as the data-desk value under Share > Embed bot in your FrontDesk dashboard.', 'frontdesk-chat' ), '123e4567-e89b-12d3-a456-426614174000' ); fdchat_select_row( 'fdchat_float', __( 'Floating button', 'frontdesk-chat' ), array( 'off' => __( 'Off', 'frontdesk-chat' ), 'badge' => __( 'Booking button', 'frontdesk-chat' ), 'chat' => __( 'Chat bubble', 'frontdesk-chat' ), ), fdchat_sanitize_float( get_option( 'fdchat_float', FDCHAT_DEFAULT_FLOAT ) ) ); fdchat_text_row( 'fdchat_text', __( 'Button text', 'frontdesk-chat' ), __( 'Label on the floating button. Up to 40 characters.', 'frontdesk-chat' ), FDCHAT_DEFAULT_TEXT ); fdchat_text_row( 'fdchat_color', __( 'Button color', 'frontdesk-chat' ), __( 'A hex color such as #2563eb. Leave empty for the default.', 'frontdesk-chat' ), FDCHAT_DEFAULT_COLOR ); fdchat_text_row( 'fdchat_text_color', __( 'Button text color', 'frontdesk-chat' ), __( 'A hex color such as #ffffff. Leave empty for the default.', 'frontdesk-chat' ), FDCHAT_DEFAULT_TEXT_COLOR ); fdchat_select_row( 'fdchat_position', __( 'Button position', 'frontdesk-chat' ), array( 'right' => __( 'Right', 'frontdesk-chat' ), 'left' => __( 'Left', 'frontdesk-chat' ), ), fdchat_sanitize_position( get_option( 'fdchat_position', FDCHAT_DEFAULT_POSITION ) ) ); echo ''; submit_button(); echo '
'; fdchat_render_consent_help(); echo '
'; } /* ------------------------------------------------------------------------ * Front-end output. PHP never contacts FrontDesk. The inline loader does so * only after the consent gate fires; until then the page carries placeholders. * --------------------------------------------------------------------- */ function fdchat_loader_js() { return <<<'JS' function fdchatLoadScript( src, data ) { var script = document.createElement( 'script' ); script.src = src; script.async = true; Object.keys( data ).forEach( function ( key ) { script.setAttribute( 'data-' + key, data[ key ] ); } ); document.body.appendChild( script ); } // Consent gate: nothing above runs until the site's consent manager says the visitor agreed. // Without a signal nothing loads. Later signals are ignored. var fdchatLoaded = false; function fdchatLoad() { if ( fdchatLoaded ) { return; } fdchatLoaded = true; var cfg = fdchatConfig.floating; var badge = cfg && 'badge' === cfg.float; // One script draws every booking surface: placeholders on the page and the floating badge. if ( badge || document.querySelector( '.fdchat-embed' ) ) { fdchatLoadScript( fdchatConfig.embed, badge ? { 'badge-src': cfg.booking, 'badge-text': cfg.text, 'badge-color': cfg.color, 'badge-text-color': cfg.textColor, 'badge-position': cfg.position } : {} ); } if ( cfg && 'chat' === cfg.float ) { fdchatLoadScript( cfg.src, { app: cfg.app, desk: cfg.desk, color: cfg.color, position: cfg.position } ); } } if ( true === window.fdchatConsentGranted ) { fdchatLoad(); } document.addEventListener( 'fdchat_consent_granted', fdchatLoad ); JS; } function fdchat_loader_script( $payload ) { return '( function ( fdchatConfig ) {' . "\n" . fdchat_loader_js() . "\n" . '} )( ' . wp_json_encode( $payload ) . ' );'; } /** * Registers the loader once, then enqueues it. Safe to call from any point: block themes render * post content before wp_head(), and WordPress ignores enqueue calls for handles not yet registered. */ function fdchat_enqueue_loader() { if ( ! wp_script_is( 'fdchat-loader', 'registered' ) ) { wp_register_script( 'fdchat-loader', false, array(), FDCHAT_VERSION, true ); $payload = array( 'embed' => FDCHAT_EMBED_SRC, 'floating' => fdchat_get_config(), ); wp_add_inline_script( 'fdchat-loader', fdchat_loader_script( $payload ) ); } wp_enqueue_script( 'fdchat-loader' ); } /** * wp_enqueue_scripts is invoked from wp_head(), so this only runs on the front end. * The bubble needs the loader on every page; embeds enqueue it themselves. */ function fdchat_maybe_enqueue_bubble() { if ( null !== fdchat_get_config() ) { fdchat_enqueue_loader(); } } add_action( 'wp_enqueue_scripts', 'fdchat_maybe_enqueue_bubble' ); /** * Shared by the shortcode and the block (the block's attributes use the same names). * Prints a placeholder only: the hosted embed.js draws it after consent, as an inline page, * a text link or a button. Nothing is requested here. */ function fdchat_render_embed( $atts ) { $atts = shortcode_atts( array( 'view' => 'booking', 'display' => FDCHAT_DEFAULT_DISPLAY, 'url' => '', 'desk' => '', 'text' => '', 'color' => '', 'text_color' => '', ), $atts, 'frontdeskchat' ); $chat = 'chat' === $atts['view']; if ( $chat ) { $src = fdchat_valid_chat_url( $atts['url'] ); } else { $src = fdchat_booking_url( '' !== $atts['desk'] ? $atts['desk'] : get_option( 'fdchat_desk_id', '' ) ); } if ( '' === $src ) { return ''; } $default_text = $chat ? __( 'Chat with us', 'frontdesk-chat' ) : FDCHAT_DEFAULT_TEXT; $text = '' === trim( (string) $atts['text'] ) ? $default_text : fdchat_sanitize_text( $atts['text'] ); fdchat_enqueue_loader(); return sprintf( '
', esc_attr( fdchat_sanitize_display( $atts['display'] ) ), esc_url( $src ), esc_attr( $text ), esc_attr( fdchat_sanitize_color( $atts['color'] ) ), esc_attr( fdchat_sanitize_text_color( $atts['text_color'] ) ), esc_attr( __( 'FrontDesk Chat', 'frontdesk-chat' ) ) ); } add_shortcode( 'frontdeskchat', 'fdchat_render_embed' ); /** * Block metadata lives here; WordPress hands it to the editor, so js/block.js only adds the UI. */ function fdchat_register_block() { wp_register_script( 'fdchat-block', plugins_url( 'js/block.js', __FILE__ ), array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ), FDCHAT_VERSION, true ); register_block_type( 'frontdeskchat/embed', array( 'title' => __( 'FrontDesk Chat', 'frontdesk-chat' ), 'description' => __( 'Show your FrontDesk booking page or chat page on this page.', 'frontdesk-chat' ), 'category' => 'embed', 'icon' => 'calendar-alt', 'attributes' => array( 'view' => array( 'type' => 'string', 'default' => 'booking', ), 'display' => array( 'type' => 'string', 'default' => FDCHAT_DEFAULT_DISPLAY, ), 'url' => array( 'type' => 'string', 'default' => '', ), 'desk' => array( 'type' => 'string', 'default' => '', ), 'text' => array( 'type' => 'string', 'default' => '', ), 'color' => array( 'type' => 'string', 'default' => '', ), 'text_color' => array( 'type' => 'string', 'default' => '', ), ), 'render_callback' => 'fdchat_render_embed', 'editor_script' => 'fdchat-block', ) ); } add_action( 'init', 'fdchat_register_block' );