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 | utils/global-helper.php +517 -40 1.0.10 → 1.0.64 View file →
@@ -1,5 +1,7 @@
1 1 <?php
2 +defined( 'ABSPATH' ) || exit;
3 +
2 4 use function Timetics\timetics;
3 5 use Timetics\Core\Bookings\Booking;
4 6 use Timetics\Core\Integrations\Google\Client;
5 7 use Timetics\Core\Integrations\Google\Service\Calendar;
@@ -47,22 +49,47 @@
47 49 */
48 50 function timetics_get_google_access_token( $user_id = 0 ) {
49 51 $data = timetics_get_google_auth( $user_id );
50 52
51 - if ( ! $data ) {
53 + if ( empty( $data ) || empty( $data['access_token'] ) ) {
52 54 return false;
53 55 }
54 56
55 - if ( ( $data['expires_in'] - 30 ) < time() ) {
56 - $refresh_toekn = timetics_get_google_refresh_token( $user_id );
57 - $client = timetics_get_google_client();
57 + $expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 0;
58 58
59 - $data = $client->fetch_access_token_with_refresh_token( $refresh_toekn );
59 + // Token still valid (30s safety margin) — use it as-is.
60 + if ( ( $expires_in - 30 ) > time() ) {
61 + return $data['access_token'];
62 + }
60 63
61 - timetics_update_google_auth( $user_id, $data );
64 + // Needs a refresh.
65 + $refresh_token = timetics_get_google_refresh_token( $user_id );
66 +
67 + if ( empty( $refresh_token ) ) {
68 + return false;
62 69 }
63 70
64 - return $data['access_token'];
71 + $client = timetics_get_google_client();
72 + $response = $client->fetch_access_token_with_refresh_token( $refresh_token );
73 +
74 + if ( is_wp_error( $response ) ) {
75 + $error_data = $response->get_error_data();
76 + $transient = ! is_array( $error_data ) || ! empty( $error_data['transient'] );
77 +
78 + if ( $transient ) {
79 + return false;
80 + }
81 +
82 + // Permanent failure (invalid_grant): the refresh token is dead and the account genuinely needs to be re-connected. Flag it so the UI can reflect the real state.
83 + update_user_meta( $user_id, 'timetics_google_auth_error', $response->get_error_code() );
84 +
85 + return false;
86 + }
87 +
88 + // Persisting a fresh credential also clears any stale auth-error flag
89 + timetics_update_google_auth( $user_id, $response );
90 +
91 + return $response['access_token'];
65 92 }
66 93 }
67 94
68 95 if ( ! function_exists( 'timetics_get_google_refresh_token' ) ) {
@@ -101,8 +128,12 @@
101 128 *
102 129 * @return void
103 130 */
104 131 function timetics_update_google_auth( $user_id = 0, $data = [] ) {
132 + if ( empty( $data ) || ! is_array( $data ) ) {
133 + return;
134 + }
135 +
105 136 if ( ! empty( $data['code'] ) ) {
106 137 update_user_meta( $user_id, 'timetics_google_auth_code', $data['code'] );
107 138 }
108 139
@@ -109,11 +140,17 @@
109 140 if ( ! empty( $data['refresh_token'] ) ) {
110 141 update_user_meta( $user_id, 'timetics_google_refresh_token', $data['refresh_token'] );
111 142 }
112 143
113 - $data['expires_in'] = $data['expires_in'] + time();
144 + // Google returns expires_in as a lifetime in seconds (~3600).
145 + // Guard against a missing/zero value: storing time() alone would make the
146 + // token look permanently expired, forcing a refresh on every request and
147 + // hammering Google's token endpoint until it rate-limits the app.
148 + $lifetime = ! empty( $data['expires_in'] ) ? (int) $data['expires_in'] : 3600;
149 + $data['expires_in'] = time() + $lifetime;
114 150
115 151 update_user_meta( $user_id, 'timetics_google_auth', $data );
152 + delete_user_meta( $user_id, 'timetics_google_auth_error' );
116 153 }
117 154 }
118 155
119 156 if ( ! function_exists( 'timetics_get_google_auth' ) ) {
@@ -162,11 +199,11 @@
162 199 *
163 200 * @return string
164 201 */
165 202 function timetics_get_google_auth_url() {
166 - $client = timetics_get_google_client();
203 + $local_url = site_url( 'timetics-integration/google-auth' );
167 204
168 - return $client->get_auth_url();
205 + return wp_nonce_url( $local_url, 'timetics_auth_action', 'timetics_auth_nonce' );
169 206 }
170 207 }
171 208
172 209 if ( ! function_exists( 'timetics_get_posts' ) ) {
@@ -308,8 +345,70 @@
308 345 return $total;
309 346 }
310 347 }
311 348
349 +if ( ! function_exists( 'timetics_can_view_all_data' ) ) {
350 + /**
351 + * Check the current user is allowed to see site wide data
352 + *
353 + * Staff hold `manage_timetics`, so that capability cannot be used to tell an
354 + * administrator apart from a team member.
355 + *
356 + * @return bool
357 + */
358 + function timetics_can_view_all_data() {
359 + return current_user_can( 'manage_options' );
360 + }
361 +}
362 +
363 +if ( ! function_exists( 'timetics_get_visible_booking_ids' ) ) {
364 + /**
365 + * Get booking ids a user is allowed to see
366 + *
367 + * @param integer $user_id
368 + *
369 + * @return array Never empty, so it is always safe to pass to `post__in`.
370 + */
371 + function timetics_get_visible_booking_ids( $user_id = 0 ) {
372 + if ( ! $user_id ) {
373 + $user_id = get_current_user_id();
374 + }
375 +
376 + $booking_ids = Booking::get_visible_ids_for_user( $user_id );
377 +
378 + // WP_Query ignores an empty post__in and returns everything, so fall back to a
379 + // non-existent id to mean "nothing visible".
380 + return ! empty( $booking_ids ) ? $booking_ids : [0];
381 + }
382 +}
383 +
384 +if ( ! function_exists( 'timetics_get_visible_customer_ids' ) ) {
385 + /**
386 + * Get customer user ids a user is allowed to see, derived from their visible bookings
387 + *
388 + * @param integer $user_id
389 + *
390 + * @return array Never empty, so it is always safe to pass to `include`.
391 + */
392 + function timetics_get_visible_customer_ids( $user_id = 0 ) {
393 + $booking_ids = timetics_get_visible_booking_ids( $user_id );
394 + $customer_ids = [];
395 +
396 + foreach ( $booking_ids as $booking_id ) {
397 + $customer_id = (int) get_post_meta( $booking_id, '_tt_booking_customer', true );
398 +
399 + if ( $customer_id ) {
400 + $customer_ids[] = $customer_id;
401 + }
402 + }
403 +
404 + $customer_ids = array_values( array_unique( $customer_ids ) );
405 +
406 + // WP_User_Query ignores an empty include and returns everything.
407 + return ! empty( $customer_ids ) ? $customer_ids : [0];
408 + }
409 +}
410 +
312 411 if ( ! function_exists( 'timetics_get_staff_integrations' ) ) {
313 412 /**
314 413 * Get all staff integrations
315 414 *
@@ -315,9 +414,15 @@
315 414 *
316 415 * @return array
317 416 */
318 417 function timetics_get_staff_integrations( $user_id = 0 ) {
319 - $google_auth = get_user_meta( $user_id, 'timetics_google_auth_code', true );
418 + timetics_get_google_access_token( $user_id );
419 +
420 + $refresh_token = timetics_get_google_refresh_token( $user_id );
421 + $auth_error = get_user_meta( $user_id, 'timetics_google_auth_error', true );
422 + $is_connected = ! empty( $refresh_token ) && empty( $auth_error );
423 + $needs_reauth = ! empty( $refresh_token ) && ! empty( $auth_error );
424 +
320 425 $google_credentials = timetics_get_google_credentials();
321 426 $is_setup = ! empty( $google_credentials['client_id'] ) && ! empty( $google_credentials['client_secret'] );
322 427 $current_user_id = ( $user_id == get_current_user_id() ) ? true : false;
323 428
@@ -326,9 +431,10 @@
326 431 'id' => 'google-calendar-meet',
327 432 'name' => esc_html__( 'Google Meet', 'timetics' ),
328 433 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
329 434 'auth_url' => timetics_get_google_auth_url(),
330 - 'connected' => ! empty( $google_auth ),
435 + 'connected' => $is_connected,
436 + 'needs_reauth' => $needs_reauth,
331 437 'setup' => $is_setup,
332 438 'current_user' => $current_user_id,
333 439 ],
334 440 [
@@ -335,9 +441,10 @@
335 441 'id' => 'google-calendar',
336 442 'name' => esc_html__( 'Google Calendar', 'timetics' ),
337 443 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
338 444 'auth_url' => timetics_get_google_auth_url(),
339 - 'connected' => ! empty( $google_auth ),
445 + 'connected' => $is_connected,
446 + 'needs_reauth' => $needs_reauth,
340 447 'setup' => $is_setup,
341 448 'current_user' => $current_user_id,
342 449 ],
343 450 ];
@@ -399,12 +506,14 @@
399 506 */
400 507 function timetics_update_default_settings() {
401 508 $settings = [
402 509 'google_auth_redirect_uri' => timetics_get_auth_redirect_uri( 'google-auth' ),
510 + 'outlook_auth_redirect_uri'=> timetics_get_auth_redirect_uri( 'outlook-auth' ),
403 511 'default_booking_status' => 'pending',
404 512 'currency' => 'USD',
405 513 'primary_color' => '#3161F1',
406 514 'secondary_color' => '#6188ff',
515 + 'unpaid_booking_expiry_minutes' => 5,
407 516 ];
408 517
409 518 $settings = apply_filters( 'timetics_default_settings', $settings );
410 519
@@ -506,8 +615,14 @@
506 615 * @return void
507 616 */
508 617 function timetics_register_cron() {
509 618 wp_schedule_event( time(), 'daily', 'timetics_booking_clear_schedule' );
619 +
620 + // Guarded: Hooks::maybe_schedule_cleanup_cron() also calls this on
621 + // every 'init', so without the guard it'd stack duplicate events.
622 + if ( ! wp_next_scheduled( 'timetics_cleanup_unpaid_bookings' ) ) {
623 + wp_schedule_event( time(), 'timetics_five_minutes', 'timetics_cleanup_unpaid_bookings' );
624 + }
510 625 }
511 626 }
512 627
513 628 if ( ! function_exists( 'timetics_is_woocommerce_active' ) ) {
@@ -617,43 +732,405 @@
617 732 $datetime->setTimezone( new DateTimeZone( $to ) );
618 733
619 734 return $datetime;
620 735 }
736 +}
621 737
622 - if ( ! function_exists( 'timetics_generate_username' ) ) {
623 - /**
624 - * Generate unique username
625 - *
626 - * @param string $email
627 - *
628 - * @return string
629 - */
630 - function timetics_generate_username( $email ) {
631 - $username = strtok( $email, '@' );
738 +if ( ! function_exists( 'timetics_generate_username' ) ) {
739 + /**
740 + * Generate unique username
741 + *
742 + * @param string $email
743 + *
744 + * @return string
745 + */
746 + function timetics_generate_username( $email ) {
747 + $username = strtok( $email, '@' );
632 748
633 - if ( username_exists( $username ) ) {
634 - $username = $username . wp_rand( 10, 100 );
749 + if ( username_exists( $username ) ) {
750 + $username = $username . wp_rand( 10, 100 );
751 + }
752 +
753 + return $username;
754 + }
755 +}
756 +
757 +if ( ! function_exists( 'timetics_is_json' ) ) {
758 + /**
759 + * Check a string is valid json or not
760 + *
761 + * @param string $json_string
762 + *
763 + * @return bool
764 + */
765 + function timetics_is_json( $json_string ) {
766 + if ( ! is_string( $json_string ) ) {
767 + return false;
768 + }
769 +
770 + $data = json_decode( $json_string, true );
771 +
772 + return $data !== null && json_last_error() === JSON_ERROR_NONE;
773 + }
774 +}
775 +
776 +if ( ! function_exists( 'timetics_is_google_meet_connected' ) ) {
777 +
778 + /**
779 + * Check google meet is connected or not
780 + *
781 + * @param integer $user_id
782 + *
783 + * @return bool
784 + */
785 + function timetics_is_google_meet_connected( $user_id = 0 ) {
786 + if ( ! $user_id ) {
787 + $user_id = get_current_user_id();
788 + }
789 +
790 + $google_auth = timetics_get_google_access_token( $user_id );
791 +
792 + return ! empty( $google_auth );
793 + }
794 +}
795 +
796 +if ( ! function_exists( 'timetics_is_zoom_connected' ) ) {
797 +
798 + /**
799 + * Check google meet is connected or not
800 + *
801 + * @param integer $user_id
802 + *
803 + * @return bool
804 + */
805 + function timetics_is_zoom_connected( $user_id = 0 ) {
806 + if ( ! $user_id ) {
807 + $user_id = get_current_user_id();
808 + }
809 +
810 + if ( ! function_exists( 'timetics_get_zoom_access_token' ) ) {
811 + return false;
812 + }
813 +
814 + $zoom_auth = timetics_get_zoom_access_token( $user_id );
815 +
816 + return ! empty( $zoom_auth );
817 + }
818 +}
819 +
820 +if ( ! function_exists( 'timetics_get_current_user' ) ) {
821 + /**
822 + * Get current user data
823 + *
824 + * @return array
825 + */
826 + function timetics_get_current_user() {
827 + $user_id = get_current_user_id();
828 + $user = get_userdata( $user_id );
829 +
830 + $user_data = [
831 + 'id' => $user->ID,
832 + 'username' => $user->user_login,
833 + 'email' => $user->user_email,
834 + 'roles' => $user-> roles,
835 + ];
836 + return $user_data;
837 + }
838 +}
839 +
840 +if ( ! function_exists( 'timetics_connected_platforms' ) ) {
841 +
842 + /**
843 + * Get connected platforms
844 + *
845 + * @param integer $id
846 + *
847 + * @return array Connected platform lists
848 + */
849 + function timetics_connected_platforms( $id ) {
850 + $integrations = timetics_get_staff_integrations( $id );
851 + $integration_names = [];
852 +
853 + if ( $integrations ) {
854 + foreach ( $integrations as $integration ) {
855 + if ( $integration['connected'] ) {
856 + $integration_names[] = $integration['id'];
857 + }
635 858 }
859 + }
636 860
637 - return $username;
861 + return $integration_names;
862 + }
863 +}
864 +
865 +if ( ! function_exists( 'timetics_modules_list' ) ) {
866 + /**
867 + * Get the master list of Arraytics plugins for the About Us page.
868 + *
869 + * Each entry describes an external Arraytics plugin that can be
870 + * installed/activated/deactivated from within the Timetics admin.
871 + *
872 + * @return array
873 + */
874 + function timetics_modules_list(): array {
875 + return [
876 + 'eventin' => [
877 + 'name' => 'eventin',
878 + 'slug' => 'wp-event-solution',
879 + 'type' => 'plugin',
880 + 'upgrade' => false,
881 + 'upgrade_link' => 'https://themewinter.com/eventin',
882 + 'status' => 'on',
883 + 'is_pro' => false,
884 + 'title' => __( 'Eventin', 'timetics' ),
885 + 'description' => __( 'Best Free WordPress Event Calendar Plugin to manage booking, registration, RSVPs, schedule recurring events and sell event tickets with WooCommerce.', 'timetics' ),
886 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'eventin' ),
887 + 'notice' => '',
888 + 'demo_link' => 'https://product.themewinter.com/eventin',
889 + 'settings_link' => '',
890 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/eventin/',
891 + ],
892 + 'booktics' => [
893 + 'name' => 'booktics',
894 + 'slug' => 'booktics',
895 + 'type' => 'plugin',
896 + 'upgrade' => false,
897 + 'upgrade_link' => 'https://arraytics.com/booktics',
898 + 'status' => 'on',
899 + 'is_pro' => false,
900 + 'title' => __( 'Booktics', 'timetics' ),
901 + 'description' => __( 'The ultimate online booking plugin for WordPress — services, staff, schedules, and payments in one place.', 'timetics' ),
902 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'booktics' ),
903 + 'notice' => '',
904 + 'demo_link' => 'https://arraytics.com/booktics',
905 + 'settings_link' => '',
906 + 'doc_link' => 'https://docs.arraytics.com/docs/booktics/',
907 + ],
908 + 'poptics' => [
909 + 'name' => 'poptics',
910 + 'slug' => 'poptics',
911 + 'type' => 'plugin',
912 + 'upgrade' => false,
913 + 'upgrade_link' => 'https://arraytics.com/poptics',
914 + 'status' => 'on',
915 + 'is_pro' => false,
916 + 'title' => __( 'Poptics', 'timetics' ),
917 + 'description' => __( 'Discover the ultimate popup mix with Poptics Popup Builder to boost your lead generation and sales conversions.', 'timetics' ),
918 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'poptics' ),
919 + 'notice' => '',
920 + 'demo_link' => 'https://arraytics.com/poptics',
921 + 'settings_link' => '',
922 + 'doc_link' => 'https://docs.aethonic.com/docs/getting-started/intro/',
923 + ],
924 + 'wpcafe' => [
925 + 'name' => 'wpcafe',
926 + 'slug' => 'wp-cafe',
927 + 'type' => 'plugin',
928 + 'upgrade' => false,
929 + 'upgrade_link' => 'https://themewinter.com/wpcafe',
930 + 'status' => 'on',
931 + 'is_pro' => false,
932 + 'title' => __( 'WPCafe', 'timetics' ),
933 + 'description' => __( 'WPCafe — Restaurant plugin with Online Ordering, Delivery, Pickup, Food Menu, Reservations, and Table Management.', 'timetics' ),
934 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'wpcafe' ),
935 + 'notice' => '',
936 + 'demo_link' => 'https://product.themewinter.com/wpcafe',
937 + 'settings_link' => '',
938 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/wp-cafe/',
939 + ],
940 + 'aisentic' => [
941 + 'name' => 'aisentic',
942 + 'slug' => 'aisentic',
943 + 'type' => 'plugin',
944 + 'upgrade' => false,
945 + 'upgrade_link' => '',
946 + 'status' => 'on',
947 + 'is_pro' => false,
948 + 'title' => __( 'Aisentic', 'timetics' ),
949 + 'description' => __( 'Your AI assistant for Timetics. Manage bookings, analyze schedules and automate routine tasks using plain English — get work done in seconds instead of hours.', 'timetics' ),
950 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ),
951 + 'notice' => '',
952 + 'demo_link' => '',
953 + 'settings_link' => '',
954 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/aisentic/',
955 + ],
956 + 'optiontics' => [
957 + 'name' => 'optiontics',
958 + 'slug' => 'optiontics',
959 + 'type' => 'plugin',
960 + 'upgrade' => false,
961 + 'upgrade_link' => '',
962 + 'status' => 'on',
963 + 'is_pro' => false,
964 + 'title' => __( 'Optiontics', 'timetics' ),
965 + 'description' => __( 'Offer paid add-ons and extra options with every booking — services, extras, packages or custom upgrades that customers can pick during checkout.', 'timetics' ),
966 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'optiontics' ),
967 + 'notice' => '',
968 + 'demo_link' => '',
969 + 'settings_link' => '',
970 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/plugin-docs/optiontics/how-to-create-product-options/',
971 + 'download_url' => 'https://github.com/themewinter/optiontics-public/releases/download/release/optiontics.zip',
972 + ],
973 + ];
974 + }
975 +}
976 +
977 +if ( ! function_exists( 'timetics_extension' ) ) {
978 + /**
979 + * Get the Timetics Extension manager instance (tools-sdk).
980 + *
981 + * @return \Arraytics\ToolsSdk\Extension
982 + */
983 + function timetics_extension(): \Arraytics\ToolsSdk\Extension {
984 + return new \Arraytics\ToolsSdk\Extension( 'timetics_available_modules', timetics_modules_list() );
985 + }
986 +}
987 +
988 +if ( ! function_exists( 'timetics_get_enabled_modules' ) ) {
989 + /**
990 + * Get list of enabled module slugs.
991 + *
992 + * @return array
993 + */
994 + function timetics_get_enabled_modules(): array {
995 + $modules = get_option( 'timetics_available_modules', [] );
996 +
997 + if ( empty( $modules ) || ! is_array( $modules ) ) {
998 + return [];
638 999 }
1000 +
1001 + $enabled = [];
1002 +
1003 + foreach ( $modules as $slug => $data ) {
1004 + if ( is_array( $data ) && isset( $data['status'] ) && 'on' === $data['status'] ) {
1005 + $enabled[] = $slug;
1006 + } elseif ( is_string( $data ) && 'on' === $data ) {
1007 + $enabled[] = $slug;
1008 + }
1009 + }
1010 +
1011 + return $enabled;
639 1012 }
1013 +}
640 1014
641 - if ( ! function_exists( 'timetics_is_json' ) ) {
642 - /**
643 - * Check a string is valid json or not
644 - *
645 - * @param string $json_string
646 - *
647 - * @return bool
648 - */
649 - function timetics_is_json( $json_string ) {
650 - if ( ! is_string( $json_string ) ) {
651 - return false;
652 - }
1015 +if ( ! function_exists( 'timetics_wp_timezone_string' ) ) {
1016 + /**
1017 + * wp_timezone_string() polyfill — core's version needs WP 5.3, plugin supports 5.2.
1018 + *
1019 + * @return string
1020 + */
1021 + function timetics_wp_timezone_string() {
1022 + if ( function_exists( 'wp_timezone_string' ) ) {
1023 + return wp_timezone_string();
1024 + }
653 1025
654 - $data = json_decode( $json_string, true );
1026 + $timezone_string = get_option( 'timezone_string' );
655 1027
656 - return $data !== null && json_last_error() === JSON_ERROR_NONE;
1028 + if ( $timezone_string ) {
1029 + return $timezone_string;
657 1030 }
1031 +
1032 + $offset = (float) get_option( 'gmt_offset' );
1033 + $hours = (int) $offset;
1034 + $minutes = abs( ( $offset - $hours ) * 60 );
1035 +
1036 + return sprintf( '%s%02d:%02d', ( $offset < 0 ) ? '-' : '+', abs( $hours ), $minutes );
1037 + }
1038 +}
1039 +
1040 +if ( ! function_exists( 'timetics_reminder_fallback_timezone' ) ) {
1041 + /**
1042 + * Timezone to interpret a booking's stored wall clock in when the booking
1043 + * itself carries no usable timezone.
1044 + *
1045 + * A booking normally stores the customer's timezone next to the customer's
1046 + * local start time, and that pair must be read together. Bookings created
1047 + * before that meta existed, or from the admin side, have no timezone — those
1048 + * times were entered against the site clock, so the site timezone is the
1049 + * closer reading. Falling back to UTC put every reminder on such a booking
1050 + * out by the site's GMT offset.
1051 + *
1052 + * @return string A timezone identifier or ±hh:mm offset accepted by DateTimeZone.
1053 + */
1054 + function timetics_reminder_fallback_timezone() {
1055 + $timezone = timetics_wp_timezone_string();
1056 +
1057 + return $timezone ? $timezone : 'UTC';
1058 + }
1059 +}
1060 +
1061 +if ( ! function_exists( 'timetics_format_email_datetime' ) ) {
1062 + /**
1063 + * Format date, time and day according to WordPress admin settings
1064 + * Used in email templates to display consistent date/time formats
1065 + *
1066 + * @param string $start_date Start date (Y-m-d format)
1067 + * @param string $start_time Start time (h:i a format)
1068 + *
1069 + * @return array Array with 'day', 'time', 'date' keys
1070 + */
1071 + function timetics_format_email_datetime( $start_date, $start_time = '' ) {
1072 + $wp_date_format = get_option( 'date_format' );
1073 + $wp_time_format = get_option( 'time_format' );
1074 +
1075 + $datetime = $start_time
1076 + ? $start_date . ' ' . $start_time
1077 + : $start_date;
1078 +
1079 + $timestamp = strtotime( $datetime );
1080 +
1081 + return [
1082 + 'day' => date_i18n( 'l', $timestamp ),
1083 + 'time' => date_i18n( $wp_time_format, $timestamp ),
1084 + 'date' => date_i18n( $wp_date_format, $timestamp ),
1085 + ];
1086 + }
1087 +}
1088 +
1089 +if ( ! function_exists( 'timetics_aisentic_identity' ) ) {
1090 + /**
1091 + * Resolve the identity Timetics hands to Aisentic when the user connects.
1092 + *
1093 + * The consent UI shows these exact values before anything leaves the site,
1094 + * so the connect request must read them from here too. Showing one email
1095 + * and sending another would break the consent.
1096 + *
1097 + * The account belongs to the person who opts in, so it is the logged-in
1098 + * user's name and email. The Ask AI setup dialog lets them edit the email,
1099 + * which arrives here as $email. The site admin email is the last fallback.
1100 + *
1101 + * @param string $email Optional email the user typed in the consent UI.
1102 + * @return array{name:string,email:string,site_url:string}
1103 + */
1104 + function timetics_aisentic_identity( $email = '' ) {
1105 + $user = wp_get_current_user();
1106 + $name = sanitize_text_field( (string) $user->display_name );
1107 + $email = sanitize_email( (string) ( $email ?: $user->user_email ?: get_option( 'admin_email', '' ) ) );
1108 +
1109 + return [
1110 + 'name' => $name,
1111 + 'email' => $email,
1112 + 'site_url' => site_url(),
1113 + ];
1114 + }
1115 +}
1116 +
1117 +if ( ! function_exists( 'timetics_aisentic_is_registered' ) ) {
1118 + /**
1119 + * Whether Aisentic already holds a provider api key.
1120 + *
1121 + * Reads Aisentic's own settings, so a site the user registered from
1122 + * Aisentic's own screens counts as connected as well. Without that the
1123 + * dashboard banner would keep nagging people who are already set up.
1124 + *
1125 + * @return bool
1126 + */
1127 + function timetics_aisentic_is_registered() {
1128 + if ( function_exists( 'aisentic_get_option' ) ) {
1129 + return ! empty( aisentic_get_option( 'llmProviders.aisentic.apiKey', '' ) );
1130 + }
1131 +
1132 + $settings = get_option( 'aisentic_settings', [] );
1133 +
1134 + return ! empty( $settings['llmProviders']['aisentic']['apiKey'] );
658 1135 }
659 1136 }