initialize_constants(); self::$instance->define_tables(); self::$instance->include_files(); self::$instance->initialize_components(); return self::$instance; } /** * Return If The Main Class has Already Been Instantiated Or Not * * @since 1.0.0 * @return boolean */ private static function instantiated() { if ( ( null !== self::$instance ) && ( self::$instance instanceof Bootstrap ) ) { return true; } return false; } /** * Prepare Singleton Instance * * @since 1.0.0 * @param string $file * @return void */ private static function prepare_instance( $file = '' ) { self::$instance = new self(); self::$instance->file = $file; self::$instance->version = \Timetics::get_version(); } /** * Assets Directory URL * * @since 1.0.0 * @return void */ public function get_assets_url() { return trailingslashit( TIMETICS_PLUGIN_URL . 'assets' ); } /** * Assets Directory Path * * @since 1.0.0 * @return void */ public function get_assets_dir() { return trailingslashit( TIMETICS_PLUGIN_DIR . 'assets' ); } /** * Plugin Directory URL * * @return void */ public function get_plugin_url() { return trailingslashit( plugin_dir_url( TIMETICS_PLUGIN_FILE ) ); } /** * Plugin Directory Path * * @return void */ public function get_plugin_dir() { return \Timetics::get_plugin_dir(); } /** * Plugin Basename * * @return void */ public function get_plugin_basename() { return plugin_basename( TIMETICS_PLUGIN_FILE ); } /** * Setup Plugin Constants * * @since 1.0.0 * @return void */ private function initialize_constants() { // Plugin Version self::$instance->define( 'TIMETICS_VERSION', \Timetics::get_version() ); // Plugin Main File self::$instance->define( 'TIMETICS_PLUGIN_FILE', $this->file ); // Plugin File Basename self::$instance->define( 'TIMETICS_PLUGIN_BASE', $this->get_plugin_basename() ); // Plugin Main Directory Path self::$instance->define( 'TIMETICS_PLUGIN_DIR', $this->get_plugin_dir() ); // Plugin Main Directory URL self::$instance->define( 'TIMETICS_PLUGIN_URL', $this->get_plugin_url() ); // Plugin Assets Directory URL self::$instance->define( 'TIMETICS_ASSETS_URL', $this->get_assets_url() ); // Plugin Assets Directory Path self::$instance->define( 'TIMETICS_ASSETS_DIR', $this->get_assets_dir() ); } /** * Define constant if not already set. * * @since 1.0.0 * @param string $timetics_constant_name Constant name. * @param string|bool $value Constant value. */ private function define( $timetics_constant_name, $value ) { if ( ! defined( $timetics_constant_name ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound -- Constant name is dynamic but prefixed by caller define( $timetics_constant_name, $value ); } } /** * Define DB Tables Required For This Plugin * * @since 1.0.0 * @return void */ private function define_tables() { // To Be Implemented } /** * Include All Required Files * * @since 1.0.0 * @return void */ private function include_files() { // require_once TIMETICS_PLUGIN_DIR . 'includes/post-types.php'; // require_once TIMETICS_PLUGIN_DIR . 'includes/widgets/manifest.php'; // require_once TIMETICS_PLUGIN_DIR . 'includes/template-functions.php'; // require_once TIMETICS_PLUGIN_DIR . 'includes/shortcodes.php'; // require_once TIMETICS_PLUGIN_DIR . 'includes/install.php'; /** * Core helpers. */ require_once TIMETICS_PLUGIN_DIR . 'utils/global-helper.php'; require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/CalendarSyncFacade.php'; require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/CalendarSyncFactory.php'; require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/CalendarSyncInterface.php'; require_once TIMETICS_PLUGIN_DIR . 'core/staffs/calendars/GoogleCalendarSync.php'; } /** * Initialize All Components * * @since 1.0.0 * @return void */ private function initialize_components() { // Register scripts and styles first if ( $this->is_request( 'admin' ) ) { add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'preload_js_translations' ], 5 ); add_action( 'admin_footer', [ $this, 'admin_helpscout_beacon' ] ); } if ( $this->is_request( 'frontend' ) ) { add_action( 'wp_enqueue_scripts', array( $this, 'frontend_scripts' ) ); add_action( 'wp_enqueue_scripts', [ $this, 'preload_js_translations' ], 5 ); } // Register admin menu Core\Admin\Menu::instance()->init(); Core\Base::instance()->init(); Core\Services\Hooks::instance()->init(); Base\Custom_Endpoint::instance()->init(); global $current_screen; $this->has_pro = class_exists('TimeticsPro'); $this->handle_buy_pro_module(); if ( function_exists('WC') ) { Core\Integrations\Woocommerce\Hooks::instance()->init(); } } /** * demo url for set pro button * * @return void */ public function demo_url(){ return "https://arraytics.com/timetics/"; } /** * Preload PHP-loaded translations into wp.i18n before any bundle runs. * * Why: webpack bundles collapse many source files into one. wp_set_script_translations() * looks up a JSON file hashed from the registered bundle URL, but Loco and wp i18n make-json * shard JSON by source-file path. The hashes never match, so per-script JSON loading silently * fails. Reading the textdomain that load_text_domain() already populated and injecting it via * setLocaleData() bypasses the hash mismatch entirely — every __() call across every bundle * resolves from in-memory data set before any React component renders. */ public function preload_js_translations() { $locale = determine_locale(); $plural_forms = 'nplurals=2; plural=(n != 1);'; // Read .mo first — canonical, always-fresh source that Loco rewrites on every save. // .l10n.php cache often lags behind .mo because Loco does not regenerate it. $messages = $this->extract_mo_messages( $locale, $plural_forms ); // Merge .l10n.php on top (covers WP 6.5+ system-translated plugins where only .l10n.php exists). foreach ( [ WP_LANG_DIR . '/loco/plugins/timetics-' . $locale . '.l10n.php', TIMETICS_PLUGIN_DIR . 'languages/timetics-' . $locale . '.l10n.php', WP_LANG_DIR . '/plugins/timetics-' . $locale . '.l10n.php', ] as $file ) { if ( ! is_readable( $file ) ) { continue; } $data = include $file; if ( ! is_array( $data ) || empty( $data['messages'] ) ) { continue; } $messages = array_merge( $messages, $data['messages'] ); if ( ! empty( $data['plural-forms'] ) ) { $plural_forms = $data['plural-forms']; } } if ( empty( $messages ) ) { return; } $locale_data = [ '' => [ 'domain' => 'timetics', 'lang' => $locale, 'plural-forms' => $plural_forms, ], ]; foreach ( $messages as $msgid => $msgstr ) { if ( is_string( $msgid ) && $msgid !== '' && is_string( $msgstr ) && $msgstr !== '' ) { $locale_data[ $msgid ] = [ $msgstr ]; } } wp_add_inline_script( 'wp-i18n', sprintf( 'wp.i18n.setLocaleData( %s, "timetics" );', wp_json_encode( $locale_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ), 'after' ); } /** * Fallback: parse .mo file to extract all translations. */ private function extract_mo_messages( $locale, &$plural_forms ) { $candidates = [ WP_LANG_DIR . '/loco/plugins/timetics-' . $locale . '.mo', TIMETICS_PLUGIN_DIR . 'languages/timetics-' . $locale . '.mo', WP_LANG_DIR . '/plugins/timetics-' . $locale . '.mo', ]; $messages = []; foreach ( $candidates as $mofile ) { if ( ! is_readable( $mofile ) ) { continue; } if ( ! class_exists( 'MO' ) ) { require_once ABSPATH . WPINC . '/pomo/mo.php'; } $mo = new \MO(); if ( ! $mo->import_from_file( $mofile ) ) { continue; } if ( ! empty( $mo->headers['Plural-Forms'] ) ) { $plural_forms = $mo->headers['Plural-Forms']; } foreach ( $mo->entries as $msgid => $entry ) { if ( ! empty( $entry->translations[0] ) ) { $messages[ $msgid ] = $entry->translations[0]; } } } return $messages; } /** * Register scripts and styles for admin * * @return void */ public function admin_scripts( $handle ) { if('plugins.php' === $handle){ 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 ); wp_enqueue_script( 'timetics-feedback-modal', TIMETICS_ASSETS_URL . 'js/feedback-modal.js', [], TIMETICS_VERSION, true ); wp_register_style( 'timetics-feedback-modal', TIMETICS_ASSETS_URL . 'css/feedback-modal.css', [], TIMETICS_VERSION, 'all' ); wp_enqueue_style( 'timetics-feedback-modal', TIMETICS_ASSETS_URL . 'css/feedback-modal.css', [], TIMETICS_VERSION, 'all' ); $feedback_obj = array( 'site_url' => site_url(), 'admin_email' => get_option( 'admin_email' ) ); wp_localize_script( 'timetics-feedback-modal', 'timetics_feedback', $feedback_obj ); } if ( 'toplevel_page_timetics' !== $handle) { return; } global $wp_locale; wp_enqueue_media(); wp_enqueue_style( 'wp-color-picker' ); wp_enqueue_style( 'timetics-admin-style', TIMETICS_ASSETS_URL . 'css/admin.css', [], TIMETICS_VERSION, 'all' ); wp_enqueue_style( 'timetics-vendor', TIMETICS_ASSETS_URL . 'css/vendor.css', [], TIMETICS_VERSION, 'all' ); 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 ); wp_enqueue_script( 'timetics-packages', TIMETICS_ASSETS_URL . 'js/packages.js', [ 'timetics-flatpickr-scripts' ], TIMETICS_VERSION, true ); $calendar_locale = timetics_get_option('calendar_locale'); if (!empty($calendar_locale)) { 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 ); } 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 ); wp_set_script_translations( 'timetics-dashboard-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' ); $localize_obj = array( 'site_url' => site_url(), 'admin_url' => admin_url(), 'nonce' => wp_create_nonce( 'wp_rest' ), 'timezone' => timetics_get_timezone_list(), 'stripe_pub_key' => timetics_get_option( 'stripe_pub_key' ), 'date_format' => get_option( 'date_format' ), 'date_format_string' => date_i18n( get_option( 'date_format' ) ), 'time_format' => get_option( 'time_format' ), 'time_format_string' => date_i18n( get_option( 'time_format' ) ), 'start_of_week' => get_option( 'start_of_week', 0 ), 'default_timezone' => timetics_get_default_timezone(), 'currency_list' => timetics_get_currencies(), 'current_user_id' => get_current_user_id(), 'timeticsPro' => class_exists('TimeticsPro') ? true : false, 'demo_url' => $this->demo_url(), 'current_user' => timetics_get_current_user(), ); $localize_obj = apply_filters( 'timetics_admin_localize_data', $localize_obj ); wp_localize_script( 'timetics-dashboard-scripts', 'timetics', $localize_obj ); } /** * Register scripts and styles for frontend * * @return void */ public function frontend_scripts() { wp_register_style( 'timetics-vendor', TIMETICS_ASSETS_URL . 'css/vendor.css', [], TIMETICS_VERSION, 'all' ); wp_register_style( 'timetics-frontend', TIMETICS_ASSETS_URL . 'css/frontend.css', [], TIMETICS_VERSION, 'all' ); global $post; if ( $post ) { $is_timetics_cpt = ( 'timetics-appointment' === get_post_type( $post ) ); $timetics_shortcodes = [ 'timetics-booking-form', 'timetics-meeting-list', 'timetics-user-dashboard', 'timetics-category' ]; $has_timetics_shortcode = false; foreach ( $timetics_shortcodes as $shortcode ) { if ( has_shortcode( $post->post_content, $shortcode ) ) { $has_timetics_shortcode = true; break; } } if ( $is_timetics_cpt || $has_timetics_shortcode ) { wp_enqueue_style( 'timetics-vendor' ); wp_enqueue_style( 'timetics-frontend' ); } } 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 ); wp_enqueue_script( 'timetics-packages', TIMETICS_ASSETS_URL . 'js/packages.js', [ 'timetics-flatpickr-scripts' ], TIMETICS_VERSION, true ); 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 ); $calendar_locale = timetics_get_option('calendar_locale'); if (!empty($calendar_locale)) { 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 ); } // load text domain wp_set_script_translations( 'timetics-frontend-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' ); global $wp_locale; $stripe_pub_key = timetics_get_option( 'stripe_pub_key' ); $stripe_secret_key = timetics_get_option( 'stripe_secret_key' ); $stripe_configure = $stripe_pub_key && $stripe_secret_key; $localize_obj = array( 'site_url' => site_url(), 'nonce' => wp_create_nonce( 'wp_rest' ), 'date_format' => get_option( 'date_format' ), 'time_format' => get_option( 'time_format' ), 'stripe_pub_key' => $stripe_pub_key, 'currency' => apply_filters( 'timetics_currency', timetics_get_option( 'currency', 'USD' ) ), 'stripe_configure' => $stripe_configure, 'current_user_id' => get_current_user_id(), 'start_of_week' => get_option( 'start_of_week', 0 ), 'currency_list' => timetics_get_currencies(), 'locale_name' => strtolower( str_replace( '_', '-', get_locale() ) ) ); $localize_obj = apply_filters( 'timetics_frontned_localize_data', $localize_obj ); wp_localize_script( 'timetics-frontend-scripts', 'timetics', $localize_obj ); } /** * What type of request * * @param string $type admin,frontend, ajax, cron * @return boolean */ private function is_request( $type ) { switch ( $type ) { case 'admin': return is_admin(); case 'ajax': return defined( 'DOING_AJAX' ); case 'cron': return defined( 'DOING_CRON' ); case 'frontend': return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! $this->is_rest_api_request(); } } /** * Returns if the request is non-legacy REST API request. * * @return boolean */ private function is_rest_api_request() { $server_request = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : false; if ( ! $server_request ) { return false; } $rest_prefix = trailingslashit( rest_get_url_prefix() ); $is_rest_request = ( false !== strpos( $server_request, $rest_prefix ) ); return apply_filters( 'timetics_is_rest_api_request', $is_rest_request ); } /** * Show buy-pro menu if pro plugin not active * * @return void */ public function handle_buy_pro_module() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page parameter check, no form processing $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; if ( 'timetics' !== $page ) { return; } /** * Show banner (codename: jhanda) */ $filter_string = 'timetics,timetics-free-only'; if( $this->has_pro ) { $filter_string .= ',timetics-pro'; $filter_string = str_replace(',timetics-free-only', '', $filter_string); } \Wpmet\Libs\Banner::instance('timetics') ->is_test(true) ->set_filter(ltrim($filter_string, ',')) ->set_api_url('https://banner.themefunction.com/public/jhanda') ->set_plugin_screens('timetics') ->set_plugin_screens('toplevel_page_timetics') ->call(); // show get-help and upgrade-to-premium menu. // $this->handle_get_help_and_upgrade_menu(); } /** * Add HelpScout script to admin footer * * @return void */ public function admin_helpscout_beacon() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $is_timetics_screen = false; if ( $screen ) { $screen_id = isset( $screen->id ) ? $screen->id : ''; // Check: only load on Timetics top-level admin page if ( 'toplevel_page_timetics' === $screen_id ) { $is_timetics_screen = true; } } // Allow overriding detection via filter. $is_timetics_screen = apply_filters( 'timetics_is_admin_screen', $is_timetics_screen, $screen ); if ( ! $is_timetics_screen ) { return; } ?>