| 1 |
<?php |
| 2 |
if ( ! defined( 'ABSPATH' ) ) { |
| 3 |
exit; |
| 4 |
} |
| 5 |
|
| 6 |
/** |
| 7 |
* |
| 8 |
* @package Wa_Notifier |
| 9 |
*/ |
| 10 |
class Notifier_Frontend { |
| 11 |
/** |
| 12 |
* Init |
| 13 |
*/ |
| 14 |
public static function init() { |
| 15 |
add_action( 'wp_footer', array( __CLASS__, 'display_whatsapp_chat_button' ) ); |
| 16 |
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'add_chat_button_inline_styles' ), 20 ); |
| 17 |
} |
| 18 |
|
| 19 |
/** |
| 20 |
* Add position and device visibility CSS for the chat button. |
| 21 |
* |
| 22 |
* Device visibility is handled with media queries (not wp_is_mobile()) so it |
| 23 |
* works with page caching and can distinguish tablets. |
| 24 |
*/ |
| 25 |
public static function add_chat_button_inline_styles() { |
| 26 |
$handle = NOTIFIER_NAME . '-frontend-css'; |
| 27 |
if ( ! wp_style_is( $handle, 'enqueued' ) ) { |
| 28 |
return; |
| 29 |
} |
| 30 |
|
| 31 |
$css = ''; |
| 32 |
$position = get_option( 'notifier_ctc_position', 'bottom-right' ); |
| 33 |
$offset_x = absint( get_option( 'notifier_ctc_offset_x', 20 ) ); |
| 34 |
$offset_y = absint( get_option( 'notifier_ctc_offset_y', 20 ) ); |
| 35 |
|
| 36 |
// The default matches frontend.css; skip it so existing theme CSS overrides of the button position keep working. |
| 37 |
if ( 'bottom-right' !== $position || 20 !== $offset_x || 20 !== $offset_y ) { |
| 38 |
$css .= self::get_position_css( $position, $offset_x, $offset_y ); |
| 39 |
} |
| 40 |
|
| 41 |
$mobile_query = '(max-width: 767px)'; |
| 42 |
|
| 43 |
if ( 'yes' === get_option( 'notifier_ctc_mobile_position_enable', 'no' ) ) { |
| 44 |
$css .= '@media ' . $mobile_query . '{' . self::get_position_css( |
| 45 |
get_option( 'notifier_ctc_mobile_position', 'bottom-right' ), |
| 46 |
get_option( 'notifier_ctc_mobile_offset_x', 20 ), |
| 47 |
get_option( 'notifier_ctc_mobile_offset_y', 20 ) |
| 48 |
) . '}'; |
| 49 |
} |
| 50 |
|
| 51 |
$devices = (array) get_option( 'notifier_ctc_devices', array( 'desktop', 'tablet', 'mobile' ) ); |
| 52 |
$media_queries = array( |
| 53 |
'mobile' => $mobile_query, |
| 54 |
'tablet' => '(min-width: 768px) and (max-width: 1024px)', |
| 55 |
'desktop' => '(min-width: 1025px)', |
| 56 |
); |
| 57 |
foreach ( $media_queries as $device => $query ) { |
| 58 |
if ( ! in_array( $device, $devices, true ) ) { |
| 59 |
$css .= '@media ' . $query . '{.notifier-click-to-chat-btn{display:none !important;}}'; |
| 60 |
} |
| 61 |
} |
| 62 |
|
| 63 |
if ( '' !== $css ) { |
| 64 |
wp_add_inline_style( $handle, $css ); |
| 65 |
} |
| 66 |
} |
| 67 |
|
| 68 |
/** |
| 69 |
* Build the fixed-position CSS rule for a corner + offsets. |
| 70 |
* |
| 71 |
* @param string $position Corner key, e.g. 'bottom-right'. |
| 72 |
* @param int|string $offset_x Horizontal offset in px. |
| 73 |
* @param int|string $offset_y Vertical offset in px. |
| 74 |
* @return string |
| 75 |
*/ |
| 76 |
private static function get_position_css( $position, $offset_x, $offset_y ) { |
| 77 |
$vertical = 0 === strpos( (string) $position, 'top' ) ? 'top' : 'bottom'; |
| 78 |
$horizontal = false !== strpos( (string) $position, 'left' ) ? 'left' : 'right'; |
| 79 |
|
| 80 |
return sprintf( |
| 81 |
'.notifier-click-to-chat-btn{top:auto;bottom:auto;left:auto;right:auto;%1$s:%2$dpx;%3$s:%4$dpx;}', |
| 82 |
$vertical, |
| 83 |
absint( $offset_y ), |
| 84 |
$horizontal, |
| 85 |
absint( $offset_x ) |
| 86 |
); |
| 87 |
} |
| 88 |
|
| 89 |
/** |
| 90 |
* Whether the chat button should be displayed on the current request. |
| 91 |
* |
| 92 |
* A page qualifies when it matches any "display" rule and no "exclude" rule. |
| 93 |
* |
| 94 |
* @return bool |
| 95 |
*/ |
| 96 |
public static function should_display_on_current_page() { |
| 97 |
$rules = Notifier_Backend::get_ctc_display_rules(); |
| 98 |
|
| 99 |
if ( self::matches_any_rule( $rules['exclude'] ) ) { |
| 100 |
return false; |
| 101 |
} |
| 102 |
|
| 103 |
return self::matches_any_rule( $rules['display'] ); |
| 104 |
} |
| 105 |
|
| 106 |
/** |
| 107 |
* @param array $rows Rule rows: array( 'rule' => key, 'specifics' => array ). |
| 108 |
* @return bool |
| 109 |
*/ |
| 110 |
private static function matches_any_rule( $rows ) { |
| 111 |
foreach ( (array) $rows as $row ) { |
| 112 |
if ( ! empty( $row['rule'] ) && self::rule_matches( $row['rule'], (array) ( $row['specifics'] ?? array() ) ) ) { |
| 113 |
return true; |
| 114 |
} |
| 115 |
} |
| 116 |
return false; |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Check one rule key against the current request. Keys are documented in |
| 121 |
* Notifier_Backend::get_ctc_location_options(). |
| 122 |
* |
| 123 |
* @param string $rule |
| 124 |
* @param array $specifics |
| 125 |
* @return bool |
| 126 |
*/ |
| 127 |
private static function rule_matches( $rule, $specifics = array() ) { |
| 128 |
switch ( $rule ) { |
| 129 |
case 'basic-global': |
| 130 |
return true; |
| 131 |
case 'basic-singulars': |
| 132 |
return is_singular(); |
| 133 |
case 'basic-archives': |
| 134 |
return is_archive(); |
| 135 |
case 'special-front': |
| 136 |
return is_front_page(); |
| 137 |
case 'special-blog': |
| 138 |
return is_home(); |
| 139 |
case 'special-404': |
| 140 |
return is_404(); |
| 141 |
case 'special-search': |
| 142 |
return is_search(); |
| 143 |
case 'special-date': |
| 144 |
return is_date(); |
| 145 |
case 'special-author': |
| 146 |
return is_author(); |
| 147 |
case 'wc-shop': |
| 148 |
return function_exists( 'is_shop' ) && is_shop(); |
| 149 |
case 'wc-cart': |
| 150 |
return function_exists( 'is_cart' ) && is_cart(); |
| 151 |
case 'wc-checkout': |
| 152 |
return function_exists( 'is_checkout' ) && is_checkout(); |
| 153 |
case 'wc-account': |
| 154 |
return function_exists( 'is_account_page' ) && is_account_page(); |
| 155 |
case 'wc-order-received': |
| 156 |
return function_exists( 'is_order_received_page' ) && is_order_received_page(); |
| 157 |
case 'specifics': |
| 158 |
foreach ( $specifics as $item ) { |
| 159 |
if ( self::specific_matches( $item ) ) { |
| 160 |
return true; |
| 161 |
} |
| 162 |
} |
| 163 |
return false; |
| 164 |
} |
| 165 |
|
| 166 |
// {post_type}|all, {post_type}|archive, {post_type}|tax|{taxonomy} |
| 167 |
$parts = explode( '|', $rule ); |
| 168 |
if ( count( $parts ) < 2 || ! post_type_exists( $parts[0] ) ) { |
| 169 |
return false; |
| 170 |
} |
| 171 |
$post_type = $parts[0]; |
| 172 |
|
| 173 |
if ( 'all' === $parts[1] ) { |
| 174 |
return is_singular( $post_type ); |
| 175 |
} |
| 176 |
if ( 'archive' === $parts[1] ) { |
| 177 |
return is_post_type_archive( $post_type ); |
| 178 |
} |
| 179 |
if ( 'tax' === $parts[1] && ! empty( $parts[2] ) ) { |
| 180 |
$taxonomy = $parts[2]; |
| 181 |
if ( 'category' === $taxonomy ) { |
| 182 |
return is_category(); |
| 183 |
} |
| 184 |
if ( 'post_tag' === $taxonomy ) { |
| 185 |
return is_tag(); |
| 186 |
} |
| 187 |
return is_tax( $taxonomy ); |
| 188 |
} |
| 189 |
|
| 190 |
return false; |
| 191 |
} |
| 192 |
|
| 193 |
/** |
| 194 |
* Match a specific item: post-{id} (that single post/page/product), tax-{id} (that |
| 195 |
* term's archive) or tax-{id}-posts (any single item that has that term). |
| 196 |
* |
| 197 |
* @param string $item |
| 198 |
* @return bool |
| 199 |
*/ |
| 200 |
private static function specific_matches( $item ) { |
| 201 |
if ( ! preg_match( '/^(post|tax)-(\d+)(-posts)?$/', (string) $item, $m ) ) { |
| 202 |
return false; |
| 203 |
} |
| 204 |
$id = (int) $m[2]; |
| 205 |
|
| 206 |
if ( 'post' === $m[1] ) { |
| 207 |
if ( is_singular() && (int) get_queried_object_id() === $id ) { |
| 208 |
return true; |
| 209 |
} |
| 210 |
// Pages WordPress / WooCommerce render as archives rather than singulars. |
| 211 |
if ( is_home() && ! is_front_page() && (int) get_option( 'page_for_posts' ) === $id ) { |
| 212 |
return true; |
| 213 |
} |
| 214 |
if ( function_exists( 'is_shop' ) && function_exists( 'wc_get_page_id' ) && is_shop() && (int) wc_get_page_id( 'shop' ) === $id ) { |
| 215 |
return true; |
| 216 |
} |
| 217 |
return false; |
| 218 |
} |
| 219 |
|
| 220 |
if ( ! empty( $m[3] ) ) { |
| 221 |
$term = get_term( $id ); |
| 222 |
return $term && ! is_wp_error( $term ) && is_singular() && has_term( $id, $term->taxonomy, get_queried_object_id() ); |
| 223 |
} |
| 224 |
|
| 225 |
$queried = get_queried_object(); |
| 226 |
return ( is_category() || is_tag() || is_tax() ) && $queried instanceof WP_Term && (int) $queried->term_id === $id; |
| 227 |
} |
| 228 |
|
| 229 |
/** |
| 230 |
* Display Whatsapp chat button. |
| 231 |
*/ |
| 232 |
public static function display_whatsapp_chat_button() { |
| 233 |
$enable_click_chat = get_option( 'notifier_ctc_enable' ); |
| 234 |
$btn_style = get_option( 'notifier_ctc_button_style' ); |
| 235 |
$click_chat_number = get_option( 'notifier_ctc_whatsapp_number' ); |
| 236 |
|
| 237 |
if ( ! $btn_style ) { |
| 238 |
return; |
| 239 |
} |
| 240 |
|
| 241 |
if ( 'yes' === $enable_click_chat && ! empty( $click_chat_number ) && self::should_display_on_current_page() ) { |
| 242 |
$valid_styles = array_keys( Notifier_Backend::get_button_styles() ); |
| 243 |
if ( 'default' !== $btn_style && in_array( $btn_style, $valid_styles, true ) ) { |
| 244 |
include_once NOTIFIER_PATH . 'templates/buttons/' . $btn_style . '.php'; |
| 245 |
} |
| 246 |
} |
| 247 |
} |
| 248 |
} |
| 249 |
|