PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
← All changes | bootstrap.php +316 -13 1.0.2 → 1.0.64 View file →
@@ -13,9 +13,8 @@
13 13 *
14 14 * @since 1.0.0
15 15 */
16 16 final class Bootstrap {
17 -
18 17 /**
19 18 * @var Bootstrap The Actual Timetics instance
20 19 * @since 1.0.0
21 20 */
@@ -20,8 +19,13 @@
20 19 * @since 1.0.0
21 20 */
22 21 private static $instance;
23 22
23 +
24 + private $file;
25 + public $version;
26 + private $has_pro;
27 +
24 28 /**
25 29 * Throw Error While Trying To Clone Object
26 30 *
27 31 * @since 1.0.0
@@ -27,9 +31,9 @@
27 31 * @since 1.0.0
28 32 * @return void
29 33 */
30 34 public function __clone() {
31 - _doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'timetics' ), '1.0.0' );
35 + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cloning is forbidden.', 'timetics' ), '1.0.0' );
32 36 }
33 37
34 38 /**
35 39 * Disabling Un-serialization Of This Class
@@ -34,9 +38,9 @@
34 38 /**
35 39 * Disabling Un-serialization Of This Class
36 40 */
37 41 public function __wakeup() {
38 - _doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'timetics' ), '1.0.0' );
42 + _doing_it_wrong( __FUNCTION__, esc_html__( 'Unserializing instances of this class is forbidden.', 'timetics' ), '1.0.0' );
39 43 }
40 44
41 45 /**
42 46 * The actual TimeTics instance
@@ -170,14 +174,15 @@
170 174 /**
171 175 * Define constant if not already set.
172 176 *
173 177 * @since 1.0.0
174 - * @param string $name Constant name.
178 + * @param string $timetics_constant_name Constant name.
175 179 * @param string|bool $value Constant value.
176 180 */
177 - private function define( $name, $value ) {
178 - if ( ! defined( $name ) ) {
179 - define( $name, $value );
181 + private function define( $timetics_constant_name, $value ) {
182 + if ( ! defined( $timetics_constant_name ) ) {
183 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound -- Constant name is dynamic but prefixed by caller
184 + define( $timetics_constant_name, $value );
180 185 }
181 186 }
182 187
183 188 /**
@@ -206,8 +211,12 @@
206 211 /**
207 212 * Core helpers.
208 213 */
209 214 require_once TIMETICS_PLUGIN_DIR . 'utils/global-helper.php';
215 + require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/CalendarSyncFacade.php';
216 + require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/CalendarSyncFactory.php';
217 + require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/CalendarSyncInterface.php';
218 + require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/GoogleCalendarSync.php';
210 219 }
211 220
212 221 /**
213 222 * Initialize All Components
@@ -219,12 +228,15 @@
219 228
220 229 // Register scripts and styles first
221 230 if ( $this->is_request( 'admin' ) ) {
222 231 add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
232 + add_action( 'admin_enqueue_scripts', [ $this, 'preload_js_translations' ], 5 );
233 + add_action( 'admin_footer', [ $this, 'admin_helpscout_beacon' ] );
223 234 }
224 235
225 236 if ( $this->is_request( 'frontend' ) ) {
226 237 add_action( 'wp_enqueue_scripts', array( $this, 'frontend_scripts' ) );
238 + add_action( 'wp_enqueue_scripts', [ $this, 'preload_js_translations' ], 5 );
227 239 }
228 240
229 241 // Register admin menu
230 242 Core\Admin\Menu::instance()->init();
@@ -230,17 +242,153 @@
230 242 Core\Admin\Menu::instance()->init();
231 243 Core\Base::instance()->init();
232 244 Core\Services\Hooks::instance()->init();
233 245 Base\Custom_Endpoint::instance()->init();
246 +
247 + global $current_screen;
248 + $this->has_pro = class_exists('TimeticsPro');
249 +
250 + $this->handle_buy_pro_module();
251 +
252 + if ( function_exists('WC') ) {
253 + Core\Integrations\Woocommerce\Hooks::instance()->init();
254 + }
234 255 }
235 256
236 257 /**
258 + * demo url for set pro button
259 + *
260 + * @return void
261 + */
262 + public function demo_url(){
263 + return "https://arraytics.com/timetics/";
264 + }
265 +
266 + /**
267 + * Preload PHP-loaded translations into wp.i18n before any bundle runs.
268 + *
269 + * Why: webpack bundles collapse many source files into one. wp_set_script_translations()
270 + * looks up a JSON file hashed from the registered bundle URL, but Loco and wp i18n make-json
271 + * shard JSON by source-file path. The hashes never match, so per-script JSON loading silently
272 + * fails. Reading the textdomain that load_text_domain() already populated and injecting it via
273 + * setLocaleData() bypasses the hash mismatch entirely — every __() call across every bundle
274 + * resolves from in-memory data set before any React component renders.
275 + */
276 + public function preload_js_translations() {
277 + $locale = determine_locale();
278 + $plural_forms = 'nplurals=2; plural=(n != 1);';
279 +
280 + // Read .mo first — canonical, always-fresh source that Loco rewrites on every save.
281 + // .l10n.php cache often lags behind .mo because Loco does not regenerate it.
282 + $messages = $this->extract_mo_messages( $locale, $plural_forms );
283 +
284 + // Merge .l10n.php on top (covers WP 6.5+ system-translated plugins where only .l10n.php exists).
285 + foreach ( [
286 + WP_LANG_DIR . '/loco/plugins/timetics-' . $locale . '.l10n.php',
287 + TIMETICS_PLUGIN_DIR . 'languages/timetics-' . $locale . '.l10n.php',
288 + WP_LANG_DIR . '/plugins/timetics-' . $locale . '.l10n.php',
289 + ] as $file ) {
290 + if ( ! is_readable( $file ) ) {
291 + continue;
292 + }
293 + $data = include $file;
294 + if ( ! is_array( $data ) || empty( $data['messages'] ) ) {
295 + continue;
296 + }
297 + $messages = array_merge( $messages, $data['messages'] );
298 + if ( ! empty( $data['plural-forms'] ) ) {
299 + $plural_forms = $data['plural-forms'];
300 + }
301 + }
302 +
303 + if ( empty( $messages ) ) {
304 + return;
305 + }
306 +
307 + $locale_data = [
308 + '' => [
309 + 'domain' => 'timetics',
310 + 'lang' => $locale,
311 + 'plural-forms' => $plural_forms,
312 + ],
313 + ];
314 +
315 + foreach ( $messages as $msgid => $msgstr ) {
316 + if ( is_string( $msgid ) && $msgid !== '' && is_string( $msgstr ) && $msgstr !== '' ) {
317 + $locale_data[ $msgid ] = [ $msgstr ];
318 + }
319 + }
320 +
321 + wp_add_inline_script(
322 + 'wp-i18n',
323 + sprintf(
324 + 'wp.i18n.setLocaleData( %s, "timetics" );',
325 + wp_json_encode( $locale_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES )
326 + ),
327 + 'after'
328 + );
329 + }
330 +
331 + /**
332 + * Fallback: parse .mo file to extract all translations.
333 + */
334 + private function extract_mo_messages( $locale, &$plural_forms ) {
335 + $candidates = [
336 + WP_LANG_DIR . '/loco/plugins/timetics-' . $locale . '.mo',
337 + TIMETICS_PLUGIN_DIR . 'languages/timetics-' . $locale . '.mo',
338 + WP_LANG_DIR . '/plugins/timetics-' . $locale . '.mo',
339 + ];
340 +
341 + $messages = [];
342 +
343 + foreach ( $candidates as $mofile ) {
344 + if ( ! is_readable( $mofile ) ) {
345 + continue;
346 + }
347 + if ( ! class_exists( 'MO' ) ) {
348 + require_once ABSPATH . WPINC . '/pomo/mo.php';
349 + }
350 + $mo = new \MO();
351 + if ( ! $mo->import_from_file( $mofile ) ) {
352 + continue;
353 + }
354 + if ( ! empty( $mo->headers['Plural-Forms'] ) ) {
355 + $plural_forms = $mo->headers['Plural-Forms'];
356 + }
357 + foreach ( $mo->entries as $msgid => $entry ) {
358 + if ( ! empty( $entry->translations[0] ) ) {
359 + $messages[ $msgid ] = $entry->translations[0];
360 + }
361 + }
362 + }
363 +
364 + return $messages;
365 + }
366 +
367 + /**
237 368 * Register scripts and styles for admin
238 369 *
239 370 * @return void
240 371 */
241 372 public function admin_scripts( $handle ) {
242 - if ( 'toplevel_page_timetics' !== $handle ) {
373 +
374 + if('plugins.php' === $handle){
375 +
376 + wp_register_script( 'timetics-feedback-modal', TIMETICS_ASSETS_URL . 'js/feedback-modal.js', [ 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
377 + wp_enqueue_script( 'timetics-feedback-modal', TIMETICS_ASSETS_URL . 'js/feedback-modal.js', [], TIMETICS_VERSION, true );
378 + wp_register_style( 'timetics-feedback-modal', TIMETICS_ASSETS_URL . 'css/feedback-modal.css', [], TIMETICS_VERSION, 'all' );
379 + wp_enqueue_style( 'timetics-feedback-modal', TIMETICS_ASSETS_URL . 'css/feedback-modal.css', [], TIMETICS_VERSION, 'all' );
380 +
381 + $feedback_obj = array(
382 + 'site_url' => site_url(),
383 + 'admin_email' => get_option( 'admin_email' )
384 + );
385 +
386 + wp_localize_script( 'timetics-feedback-modal', 'timetics_feedback', $feedback_obj );
387 +
388 + }
389 +
390 + if ( 'toplevel_page_timetics' !== $handle) {
243 391 return;
244 392 }
245 393 global $wp_locale;
246 394
@@ -247,13 +395,20 @@
247 395 wp_enqueue_media();
248 396 wp_enqueue_style( 'wp-color-picker' );
249 397 wp_enqueue_style( 'timetics-admin-style', TIMETICS_ASSETS_URL . 'css/admin.css', [], TIMETICS_VERSION, 'all' );
250 398 wp_enqueue_style( 'timetics-vendor', TIMETICS_ASSETS_URL . 'css/vendor.css', [], TIMETICS_VERSION, 'all' );
251 - wp_enqueue_script( 'timetics-antd-scripts', TIMETICS_ASSETS_URL . 'js/vendors/antd.js', [ 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
252 399 wp_enqueue_script( 'timetics-flatpickr-scripts', TIMETICS_ASSETS_URL . 'js/vendors/flatpickr.js', [ 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
400 + wp_enqueue_script( 'timetics-packages', TIMETICS_ASSETS_URL . 'js/packages.js', [ 'timetics-flatpickr-scripts' ], TIMETICS_VERSION, true );
253 401
402 + $calendar_locale = timetics_get_option('calendar_locale');
403 + if (!empty($calendar_locale)) {
404 + wp_enqueue_script( 'calendar-locale', TIMETICS_ASSETS_URL . "js/local/{$calendar_locale}.js", [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
405 + }
406 +
254 407 wp_enqueue_script( 'timetics-dashboard-scripts', TIMETICS_ASSETS_URL . 'js/dashboard.js', [ 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data', 'wp-color-picker' ], TIMETICS_VERSION, true );
255 408
409 + wp_set_script_translations( 'timetics-dashboard-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' );
410 +
256 411 $localize_obj = array(
257 412 'site_url' => site_url(),
258 413 'admin_url' => admin_url(),
259 414 'nonce' => wp_create_nonce( 'wp_rest' ),
@@ -262,30 +417,92 @@
262 417 'date_format' => get_option( 'date_format' ),
263 418 'date_format_string' => date_i18n( get_option( 'date_format' ) ),
264 419 'time_format' => get_option( 'time_format' ),
265 420 'time_format_string' => date_i18n( get_option( 'time_format' ) ),
266 - 'start_of_week' => $wp_locale->get_weekday( get_option( 'start_of_week' ) ),
421 + 'start_of_week' => get_option( 'start_of_week', 0 ),
422 +
267 423 'default_timezone' => timetics_get_default_timezone(),
268 424 'currency_list' => timetics_get_currencies(),
269 425 'current_user_id' => get_current_user_id(),
270 426 'timeticsPro' => class_exists('TimeticsPro') ? true : false,
427 + 'demo_url' => $this->demo_url(),
428 + 'current_user' => timetics_get_current_user(),
429 + 'aisentic' => $this->get_aisentic_state(),
430 + 'theme_mode' => Core\Admin\Theme_Mode::get_for_localize(),
431 + 'dark_mode_enabled' => Core\Admin\Theme_Mode::is_enabled(),
271 432 );
433 +
434 + $localize_obj = apply_filters( 'timetics_admin_localize_data', $localize_obj );
435 +
272 436 wp_localize_script( 'timetics-dashboard-scripts', 'timetics', $localize_obj );
273 437 }
274 438
275 439 /**
440 + * Aisentic connect state for the onboarding consent box and dashboard banner.
441 + *
442 + * Both surfaces show the email before the user opts in, so the value comes
443 + * from the same resolver the connect request uses.
444 + *
445 + * @return array Aisentic identity plus active/registered flags.
446 + */
447 + private function get_aisentic_state() {
448 + $identity = timetics_aisentic_identity();
449 +
450 + return [
451 + 'name' => $identity['name'],
452 + 'email' => $identity['email'],
453 + 'active' => class_exists( 'Aisentic\Init' ),
454 + 'registered' => timetics_aisentic_is_registered(),
455 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ),
456 + /*
457 + * Aisentic builds before the registration handshake have no
458 + * listener for our hook. Connecting would fail silently there, so
459 + * the UI hides the offer instead of promising something dead.
460 + */
461 + 'supports_registration' => class_exists( 'Aisentic\Api\Services\Registration_Service' ),
462 + 'terms_url' => 'https://arraytics.com/terms-of-service/',
463 + // Only people who could install Aisentic by hand get the setup dialog.
464 + 'can_setup' => current_user_can( 'manage_options' ) && current_user_can( 'install_plugins' ),
465 + ];
466 + }
467 +
468 + /**
276 469 * Register scripts and styles for frontend
277 470 *
278 471 * @return void
279 472 */
280 473 public function frontend_scripts() {
474 +
281 475 wp_register_style( 'timetics-vendor', TIMETICS_ASSETS_URL . 'css/vendor.css', [], TIMETICS_VERSION, 'all' );
282 476 wp_register_style( 'timetics-frontend', TIMETICS_ASSETS_URL . 'css/frontend.css', [], TIMETICS_VERSION, 'all' );
283 477
284 - wp_register_script( 'timetics-antd-scripts', TIMETICS_ASSETS_URL . 'js/vendors/antd.js', [ 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
478 + global $post;
479 + if ( $post ) {
480 + $is_timetics_cpt = ( 'timetics-appointment' === get_post_type( $post ) );
481 + $timetics_shortcodes = [ 'timetics-booking-form', 'timetics-meeting-list', 'timetics-user-dashboard', 'timetics-category' ];
482 + $has_timetics_shortcode = false;
483 + foreach ( $timetics_shortcodes as $shortcode ) {
484 + if ( has_shortcode( $post->post_content, $shortcode ) ) {
485 + $has_timetics_shortcode = true;
486 + break;
487 + }
488 + }
489 + if ( $is_timetics_cpt || $has_timetics_shortcode ) {
490 + wp_enqueue_style( 'timetics-vendor' );
491 + wp_enqueue_style( 'timetics-frontend' );
492 + }
493 + }
494 +
285 495 wp_register_script( 'timetics-flatpickr-scripts', TIMETICS_ASSETS_URL . 'js/vendors/flatpickr.js', [ 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
496 + wp_enqueue_script( 'timetics-packages', TIMETICS_ASSETS_URL . 'js/packages.js', [ 'timetics-flatpickr-scripts' ], TIMETICS_VERSION, true );
497 +
286 498 wp_register_script( 'timetics-frontend-scripts', TIMETICS_ASSETS_URL . 'js/frontend.js', [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
287 499
500 + $calendar_locale = timetics_get_option('calendar_locale');
501 + if (!empty($calendar_locale) && file_exists( TIMETICS_PLUGIN_DIR . "assets/js/local/{$calendar_locale}.js" ) ) {
502 + wp_register_script( 'calendar-locale', TIMETICS_ASSETS_URL . "js/local/{$calendar_locale}.js", [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
503 + }
504 +
288 505 // load text domain
289 506 wp_set_script_translations( 'timetics-frontend-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' );
290 507
291 508 global $wp_locale;
@@ -295,16 +512,23 @@
295 512 $stripe_configure = $stripe_pub_key && $stripe_secret_key;
296 513
297 514 $localize_obj = array(
298 515 'site_url' => site_url(),
516 + 'nonce' => wp_create_nonce( 'wp_rest' ),
299 517 'date_format' => get_option( 'date_format' ),
300 518 'time_format' => get_option( 'time_format' ),
301 519 'stripe_pub_key' => $stripe_pub_key,
302 - 'currency' => timetics_get_option( 'currency', 'USD' ),
520 + 'currency' => apply_filters( 'timetics_currency', timetics_get_option( 'currency', 'USD' ) ),
303 521 'stripe_configure' => $stripe_configure,
304 - 'start_of_week' => $wp_locale->get_weekday( get_option( 'start_of_week' ) ),
522 + 'current_user_id' => get_current_user_id(),
523 + 'start_of_week' => get_option( 'start_of_week', 0 ),
524 + 'currency_list' => timetics_get_currencies(),
525 + 'locale_name' => strtolower( str_replace( '_', '-', get_locale() ) )
305 526
306 527 );
528 +
529 + $localize_obj = apply_filters( 'timetics_frontned_localize_data', $localize_obj );
530 +
307 531 wp_localize_script( 'timetics-frontend-scripts', 'timetics', $localize_obj );
308 532 }
309 533
310 534 /**
@@ -342,8 +566,87 @@
342 566 $is_rest_request = ( false !== strpos( $server_request, $rest_prefix ) );
343 567
344 568 return apply_filters( 'timetics_is_rest_api_request', $is_rest_request );
345 569 }
570 +
571 +
572 + /**
573 + * Show buy-pro menu if pro plugin not active
574 + *
575 + * @return void
576 + */
577 + public function handle_buy_pro_module() {
578 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page parameter check, no form processing
579 + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
580 +
581 +
582 + if ( 'timetics' !== $page ) {
583 + return;
584 + }
585 +
586 +
587 + /**
588 + * Show banner (codename: jhanda)
589 + */
590 + $filter_string = 'timetics,timetics-free-only';
591 +
592 + if( $this->has_pro ) {
593 +
594 + $filter_string .= ',timetics-pro';
595 + $filter_string = str_replace(',timetics-free-only', '', $filter_string);
596 +
597 + }
598 +
599 +
600 + \Wpmet\Libs\Banner::instance('timetics')
601 + ->is_test(true)
602 + ->set_filter(ltrim($filter_string, ','))
603 + ->set_api_url('https://banner.arraytics.com/public/jhanda')
604 + ->set_plugin_screens('timetics')
605 + ->set_plugin_screens('toplevel_page_timetics')
606 + ->call();
607 + // show get-help and upgrade-to-premium menu.
608 + // $this->handle_get_help_and_upgrade_menu();
609 + }
610 +
611 + /**
612 + * Add HelpScout script to admin footer
613 + *
614 + * @return void
615 + */
616 + public function admin_helpscout_beacon() {
617 + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
618 + $is_timetics_screen = false;
619 +
620 + if ( $screen ) {
621 + $screen_id = isset( $screen->id ) ? $screen->id : '';
622 +
623 + // Check: only load on Timetics top-level admin page
624 + if ( 'toplevel_page_timetics' === $screen_id ) {
625 + $is_timetics_screen = true;
626 + }
627 + }
628 +
629 + // Allow overriding detection via filter.
630 + $is_timetics_screen = apply_filters( 'timetics_is_admin_screen', $is_timetics_screen, $screen );
631 +
632 + if ( ! $is_timetics_screen ) {
633 + return;
634 + }
635 + ?>
636 + <script type="text/javascript">!function(e,t,n){function a(){var e=t.getElementsByTagName("script")[0],n=t.createElement("script");n.type="text/javascript",n.async=!0,n.src="https://beacon-v2.helpscout.net",e.parentNode.insertBefore(n,e)}if(e.Beacon=n=function(t,n,a){e.Beacon.readyQueue.push({method:t,options:n,data:a})},n.readyQueue=[],"complete"===t.readyState)a();else if(e.attachEvent)e.attachEvent("onload",a);else e.addEventListener("load",a,!1)}(window,document,window.Beacon||function(){});</script>
637 + <script type="text/javascript">
638 + window.Beacon('config', {
639 + color: "#0060e5",
640 + });
641 + window.Beacon('init', '4be24aa2-ee50-483c-a90c-db4216664d65');
642 + window.Beacon('on', 'ready', function(){
643 + window.Beacon('close');
644 + });
645 +
646 + </script>
647 + <?php
648 + }
346 649
347 650 }
348 651
349 652 /**