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 +247 -17 1.0.14 → 1.0.64 View file →
@@ -22,8 +22,9 @@
22 22
23 23
24 24 private $file;
25 25 public $version;
26 + private $has_pro;
26 27
27 28 /**
28 29 * Throw Error While Trying To Clone Object
29 30 *
@@ -173,14 +174,15 @@
173 174 /**
174 175 * Define constant if not already set.
175 176 *
176 177 * @since 1.0.0
177 - * @param string $name Constant name.
178 + * @param string $timetics_constant_name Constant name.
178 179 * @param string|bool $value Constant value.
179 180 */
180 - private function define( $name, $value ) {
181 - if ( ! defined( $name ) ) {
182 - 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 );
183 185 }
184 186 }
185 187
186 188 /**
@@ -209,8 +211,12 @@
209 211 /**
210 212 * Core helpers.
211 213 */
212 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';
213 219 }
214 220
215 221 /**
216 222 * Initialize All Components
@@ -222,12 +228,15 @@
222 228
223 229 // Register scripts and styles first
224 230 if ( $this->is_request( 'admin' ) ) {
225 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' ] );
226 234 }
227 235
228 236 if ( $this->is_request( 'frontend' ) ) {
229 237 add_action( 'wp_enqueue_scripts', array( $this, 'frontend_scripts' ) );
238 + add_action( 'wp_enqueue_scripts', [ $this, 'preload_js_translations' ], 5 );
230 239 }
231 240
232 241 // Register admin menu
233 242 Core\Admin\Menu::instance()->init();
@@ -235,8 +244,9 @@
235 244 Core\Services\Hooks::instance()->init();
236 245 Base\Custom_Endpoint::instance()->init();
237 246
238 247 global $current_screen;
248 + $this->has_pro = class_exists('TimeticsPro');
239 249
240 250 $this->handle_buy_pro_module();
241 251
242 252 if ( function_exists('WC') ) {
@@ -253,14 +263,132 @@
253 263 return "https://arraytics.com/timetics/";
254 264 }
255 265
256 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 + /**
257 368 * Register scripts and styles for admin
258 369 *
259 370 * @return void
260 371 */
261 372 public function admin_scripts( $handle ) {
262 - 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) {
263 391 return;
264 392 }
265 393 global $wp_locale;
266 394
@@ -267,18 +395,19 @@
267 395 wp_enqueue_media();
268 396 wp_enqueue_style( 'wp-color-picker' );
269 397 wp_enqueue_style( 'timetics-admin-style', TIMETICS_ASSETS_URL . 'css/admin.css', [], TIMETICS_VERSION, 'all' );
270 398 wp_enqueue_style( 'timetics-vendor', TIMETICS_ASSETS_URL . 'css/vendor.css', [], TIMETICS_VERSION, 'all' );
271 - 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 );
272 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 );
273 401
274 402 $calendar_locale = timetics_get_option('calendar_locale');
275 403 if (!empty($calendar_locale)) {
276 - wp_enqueue_script( 'calendar-locale', "https://npmcdn.com/flatpickr/dist/l10n/{$calendar_locale}.js", [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
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 );
277 405 }
278 406
279 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 );
280 408
409 + wp_set_script_translations( 'timetics-dashboard-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' );
281 410
282 411 $localize_obj = array(
283 412 'site_url' => site_url(),
284 413 'admin_url' => admin_url(),
@@ -296,8 +425,11 @@
296 425 'current_user_id' => get_current_user_id(),
297 426 'timeticsPro' => class_exists('TimeticsPro') ? true : false,
298 427 'demo_url' => $this->demo_url(),
299 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(),
300 432 );
301 433
302 434 $localize_obj = apply_filters( 'timetics_admin_localize_data', $localize_obj );
303 435
@@ -304,23 +436,71 @@
304 436 wp_localize_script( 'timetics-dashboard-scripts', 'timetics', $localize_obj );
305 437 }
306 438
307 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 + /**
308 469 * Register scripts and styles for frontend
309 470 *
310 471 * @return void
311 472 */
312 473 public function frontend_scripts() {
474 +
313 475 wp_register_style( 'timetics-vendor', TIMETICS_ASSETS_URL . 'css/vendor.css', [], TIMETICS_VERSION, 'all' );
314 476 wp_register_style( 'timetics-frontend', TIMETICS_ASSETS_URL . 'css/frontend.css', [], TIMETICS_VERSION, 'all' );
315 477
316 - 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 +
317 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 +
318 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 );
319 499
320 500 $calendar_locale = timetics_get_option('calendar_locale');
321 - if (!empty($calendar_locale)) {
322 - wp_register_script( 'calendar-locale', "https://npmcdn.com/flatpickr/dist/l10n/{$calendar_locale}.js", [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true );
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 );
323 503 }
324 504
325 505 // load text domain
326 506 wp_set_script_translations( 'timetics-frontend-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' );
@@ -340,8 +520,10 @@
340 520 'currency' => apply_filters( 'timetics_currency', timetics_get_option( 'currency', 'USD' ) ),
341 521 'stripe_configure' => $stripe_configure,
342 522 'current_user_id' => get_current_user_id(),
343 523 'start_of_week' => get_option( 'start_of_week', 0 ),
524 + 'currency_list' => timetics_get_currencies(),
525 + 'locale_name' => strtolower( str_replace( '_', '-', get_locale() ) )
344 526
345 527 );
346 528
347 529 $localize_obj = apply_filters( 'timetics_frontned_localize_data', $localize_obj );
@@ -392,30 +574,78 @@
392 574 *
393 575 * @return void
394 576 */
395 577 public function handle_buy_pro_module() {
396 - $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
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'] ) ) : '';
397 580
581 +
398 582 if ( 'timetics' !== $page ) {
399 583 return;
400 584 }
401 585
586 +
402 587 /**
403 588 * Show banner (codename: jhanda)
404 589 */
405 590 $filter_string = 'timetics,timetics-free-only';
406 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 +
407 600 \Wpmet\Libs\Banner::instance('timetics')
408 - // ->is_test(true)
601 + ->is_test(true)
409 602 ->set_filter(ltrim($filter_string, ','))
410 - ->set_api_url('https://demo.themewinter.com/public/jhanda')
411 - ->set_plugin_screens('edit-timetics')
412 - ->set_plugin_screens('timetics_page_timetics_sales_report')
413 - ->set_plugin_screens('edit-timetics-attendee')
414 - ->set_plugin_screens('timetics_page_timetics_get_help')
603 + ->set_api_url('https://banner.arraytics.com/public/jhanda')
604 + ->set_plugin_screens('timetics')
605 + ->set_plugin_screens('toplevel_page_timetics')
415 606 ->call();
416 607 // show get-help and upgrade-to-premium menu.
417 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
418 648 }
419 649
420 650 }
421 651