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;
}
?>