Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/timetics/core/admin/notification-seeder.php
+++ b/timetics/core/admin/notification-seeder.php
@@ -0,0 +1,429 @@
+<?php
+/**
+ * Seeds the 8 default notification flows on first activation.
+ *
+ * @package Timetics
+ */
+
+namespace TimeticsCoreAdmin;
+
+use EnsFlowFlow;
+
+defined( 'ABSPATH' ) || exit;
+
+class Notification_Seeder {
+
+ const SEEDED_OPTION = 'timetics_default_flows_seeded';
+ const SEEDED_VERSION = '2.0';
+
+ /**
+ * Seed the 8 default flows as drafts.
+ *
+ * The legacy email system in Settings remains the active sender; these
+ * flows ship as drafts (examples) so they never fire until a user
+ * explicitly publishes one.
+ */
+ public static function maybe_seed() {
+ self::migrate_bad_flows();
+
+ if ( get_option( self::SEEDED_OPTION ) === self::SEEDED_VERSION ) {
+ return;
+ }
+
+ $existing = self::existing_flow_titles();
+
+ // Demote previously published default seeds so they stop firing.
+ self::draft_published_seeds();
+
+ foreach ( self::get_default_flows() as $flow_def ) {
+ // Skip if a flow with this name already exists (avoids duplicates
+ // on upgrade); user-renamed flows are left untouched.
+ if ( in_array( $flow_def['name'], $existing, true ) ) {
+ continue;
+ }
+ self::create_flow( $flow_def );
+ }
+
+ update_option( self::SEEDED_OPTION, self::SEEDED_VERSION, false );
+ }
+
+ /**
+ * Titles of all tt-flow posts currently in the DB (any status).
+ *
+ * @return string[]
+ */
+ private static function existing_flow_titles() {
+ $posts = get_posts( array(
+ 'post_type' => 'tt-flow',
+ 'posts_per_page' => -1,
+ 'post_status' => 'any',
+ ) );
+
+ return wp_list_pluck( $posts, 'post_title' );
+ }
+
+ /**
+ * Demote any published flow whose title matches a default seed name back
+ * to draft, so the legacy Settings emails remain the sole sender.
+ */
+ private static function draft_published_seeds() {
+ $seed_names = wp_list_pluck( self::get_default_flows(), 'name' );
+
+ $posts = get_posts( array(
+ 'post_type' => 'tt-flow',
+ 'posts_per_page' => -1,
+ 'post_status' => 'publish',
+ 'fields' => 'ids',
+ ) );
+
+ foreach ( $posts as $post_id ) {
+ if ( in_array( get_the_title( $post_id ), $seed_names, true ) ) {
+ wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) );
+ }
+ }
+ }
+
+ /**
+ * Delete any tt-flow posts whose nodes are missing the 'type' field —
+ * those were created by the v1.0 seeder with the wrong format.
+ * User-created flows always have 'type' on every node (set by the UI).
+ * Also resets the seeded flag so maybe_seed() re-creates correct flows.
+ */
+ private static function migrate_bad_flows() {
+ if ( get_option( self::SEEDED_OPTION ) === self::SEEDED_VERSION ) {
+ return;
+ }
+
+ $posts = get_posts( array(
+ 'post_type' => 'tt-flow',
+ 'posts_per_page' => -1,
+ 'post_status' => 'any',
+ 'fields' => 'ids',
+ ) );
+
+ if ( empty( $posts ) ) {
+ return;
+ }
+
+ $meta_key = '_tt_notification_flow_flow_config';
+
+ foreach ( $posts as $post_id ) {
+ $config = get_post_meta( $post_id, $meta_key, true );
+
+ if ( empty( $config['nodes'] ) || ! is_array( $config['nodes'] ) ) {
+ continue;
+ }
+
+ foreach ( $config['nodes'] as $node ) {
+ if ( ! isset( $node['type'] ) ) {
+ wp_delete_post( $post_id, true );
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Force re-seed (e.g., for testing). Clears flag then re-runs.
+ */
+ public static function reseed() {
+ delete_option( self::SEEDED_OPTION );
+ self::maybe_seed();
+ }
+
+ // -------------------------------------------------------------------------
+ // Private helpers
+ // -------------------------------------------------------------------------
+
+ private static function create_flow( array $def ) {
+ $flow = new Flow( 'tt' );
+ $flow->set_props( array(
+ 'name' => $def['name'],
+ 'trigger' => $def['trigger'],
+ 'flow_config' => $def['flow_config'],
+ 'status' => 'draft',
+ ) );
+ $flow->save();
+ }
+
+ /** Edge with correct UI format. */
+ private static function edge( string $from, string $to ) {
+ return array(
+ 'id' => 'edge_' . $from . '-' . $to,
+ 'type' => 'smoothstep',
+ 'markerEnd' => array( 'type' => 'arrowclosed' ),
+ 'source' => $from,
+ 'target' => $to,
+ 'data' => array( 'animated' => false ),
+ );
+ }
+
+ /** Trigger node. */
+ private static function trigger_node( string $trigger_value, string $trigger_label ) {
+ return array(
+ 'id' => 'node_1',
+ 'type' => 'trigger',
+ 'name' => 'trigger',
+ 'position' => array( 'x' => 275, 'y' => 70 ),
+ 'data' => array(
+ 'label' => 'trigger: ' . $trigger_value,
+ 'subtitle' => 'On "' . $trigger_label . '" event fires',
+ 'triggerValue' => $trigger_value,
+ ),
+ );
+ }
+
+ /** End node. */
+ private static function end_node( float $y = 600.0 ) {
+ return array(
+ 'id' => 'end_1',
+ 'type' => 'end',
+ 'name' => 'end',
+ 'position' => array( 'x' => 275, 'y' => $y ),
+ 'data' => array(
+ 'label' => 'end_flow',
+ 'subtitle' => 'Automation stops here',
+ ),
+ );
+ }
+
+ /** Email action node. */
+ private static function email_node( string $node_id, string $receiver_type, string $receiver_label, string $subject, string $body, float $y = 420.0 ) {
+ return array(
+ 'id' => $node_id,
+ 'type' => 'action',
+ 'name' => 'email',
+ 'position' => array( 'x' => 275, 'y' => $y ),
+ 'data' => array(
+ 'actionType' => 'send_email',
+ 'label' => 'send_email',
+ 'subtitle' => 'To: ' . $receiver_label,
+ 'receiverType' => $receiver_type,
+ 'from' => '',
+ 'subject' => $subject,
+ 'body' => $body,
+ ),
+ );
+ }
+
+ /** Delay action node. */
+ private static function delay_node( string $node_id, int $delay, string $delay_unit, string $delay_condition, float $y = 245.0 ) {
+ $subtitle = 'Wait for ' . $delay . ' ' . $delay_unit;
+ if ( $delay_condition ) {
+ $subtitle .= ' before ' . str_replace( '_', ' ', str_replace( 'before_', '', $delay_condition ) );
+ }
+
+ return array(
+ 'id' => $node_id,
+ 'type' => 'action',
+ 'name' => 'delay',
+ 'position' => array( 'x' => 275, 'y' => $y ),
+ 'data' => array(
+ 'actionType' => 'add_delay',
+ 'label' => 'add_delay',
+ 'subtitle' => $subtitle,
+ 'delay' => $delay,
+ 'delayUnit' => $delay_unit,
+ 'delayCondition' => $delay_condition,
+ ),
+ );
+ }
+
+ /**
+ * trigger → email → end
+ */
+ private static function simple_flow( string $trigger_value, string $trigger_label, string $receiver_type, string $receiver_label, string $subject, string $body ) {
+ return array(
+ 'nodes' => array(
+ self::trigger_node( $trigger_value, $trigger_label ),
+ self::email_node( 'node_2', $receiver_type, $receiver_label, $subject, $body, 280.0 ),
+ self::end_node( 490.0 ),
+ ),
+ 'edges' => array(
+ self::edge( 'node_1', 'node_2' ),
+ self::edge( 'node_2', 'end_1' ),
+ ),
+ );
+ }
+
+ /**
+ * trigger → delay → email → end
+ */
+ private static function reminder_flow( string $trigger_value, string $trigger_label, string $receiver_type, string $receiver_label, string $subject, string $body ) {
+ return array(
+ 'nodes' => array(
+ self::trigger_node( $trigger_value, $trigger_label ),
+ self::delay_node( 'node_2', 24, 'hours', 'before_meeting_date', 245.0 ),
+ self::email_node( 'node_3', $receiver_type, $receiver_label, $subject, $body, 420.0 ),
+ self::end_node( 600.0 ),
+ ),
+ 'edges' => array(
+ self::edge( 'node_1', 'node_2' ),
+ self::edge( 'node_2', 'node_3' ),
+ self::edge( 'node_3', 'end_1' ),
+ ),
+ );
+ }
+
+ /**
+ * Definitions for all 8 default flows.
+ */
+ private static function get_default_flows() {
+ $booking_tags = implode( '', array(
+ '<table style="border-collapse:collapse;width:100%;margin:20px 0;">',
+ '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;width:40%;">Meeting</td><td style="padding:8px 0;color:#556880;">{%meeting_title%}</td></tr>',
+ '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Date</td><td style="padding:8px 0;color:#556880;">{%meeting_date%}</td></tr>',
+ '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Time</td><td style="padding:8px 0;color:#556880;">{%meeting_time%}</td></tr>',
+ '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Duration</td><td style="padding:8px 0;color:#556880;">{%meeting_duration%}</td></tr>',
+ '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Location</td><td style="padding:8px 0;color:#556880;">{%meeting_location%}</td></tr>',
+ ) );
+
+ return array(
+
+ // ── Booking Confirmed ──────────────────────────────────────────
+ array(
+ 'name' => 'Booking Confirmed – Customer',
+ 'trigger' => 'booking_created',
+ 'flow_config' => self::simple_flow(
+ 'booking_created',
+ 'After Booking Confirmation',
+ 'customer_email',
+ 'Customer Email',
+ 'Booking Confirmed: {%meeting_title%}',
+ '<p>Hi {%customer_name%},</p><p>Your booking has been confirmed.</p>'
+ . $booking_tags
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Host</td><td style="padding:8px 0;color:#556880;">{%host_name%}</td></tr>'
+ . '</table><p>Thank you!</p>'
+ ),
+ ),
+
+ array(
+ 'name' => 'Booking Confirmed – Host',
+ 'trigger' => 'booking_created',
+ 'flow_config' => self::simple_flow(
+ 'booking_created',
+ 'After Booking Confirmation',
+ 'host_email',
+ 'Host Email',
+ 'New Booking: {%meeting_title%}',
+ '<p>Hi {%host_name%},</p><p>A new booking has been made.</p>'
+ . $booking_tags
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Customer</td><td style="padding:8px 0;color:#556880;">{%customer_name%} ({%customer_email%})</td></tr>'
+ . '</table><p>Thank you!</p>'
+ ),
+ ),
+
+ // ── Booking Canceled ───────────────────────────────────────────
+ array(
+ 'name' => 'Booking Canceled – Customer',
+ 'trigger' => 'booking_canceled',
+ 'flow_config' => self::simple_flow(
+ 'booking_canceled',
+ 'After Booking Cancellation',
+ 'customer_email',
+ 'Customer Email',
+ 'Booking Canceled: {%meeting_title%}',
+ '<p>Hi {%customer_name%},</p><p>Your booking has been canceled.</p>'
+ . '<table style="border-collapse:collapse;width:100%;margin:20px 0;">'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;width:40%;">Meeting</td><td style="padding:8px 0;color:#556880;">{%meeting_title%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Date</td><td style="padding:8px 0;color:#556880;">{%meeting_date%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Time</td><td style="padding:8px 0;color:#556880;">{%meeting_time%}</td></tr>'
+ . '</table><p>If you have questions, please contact us.</p><p>Thank you!</p>'
+ ),
+ ),
+
+ array(
+ 'name' => 'Booking Canceled – Host',
+ 'trigger' => 'booking_canceled',
+ 'flow_config' => self::simple_flow(
+ 'booking_canceled',
+ 'After Booking Cancellation',
+ 'host_email',
+ 'Host Email',
+ 'Booking Canceled: {%meeting_title%}',
+ '<p>Hi {%host_name%},</p><p>A booking has been canceled.</p>'
+ . '<table style="border-collapse:collapse;width:100%;margin:20px 0;">'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;width:40%;">Meeting</td><td style="padding:8px 0;color:#556880;">{%meeting_title%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Date</td><td style="padding:8px 0;color:#556880;">{%meeting_date%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Time</td><td style="padding:8px 0;color:#556880;">{%meeting_time%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Customer</td><td style="padding:8px 0;color:#556880;">{%customer_name%} ({%customer_email%})</td></tr>'
+ . '</table><p>Thank you!</p>'
+ ),
+ ),
+
+ // ── Booking Rescheduled ────────────────────────────────────────
+ array(
+ 'name' => 'Booking Rescheduled – Customer',
+ 'trigger' => 'booking_rescheduled',
+ 'flow_config' => self::simple_flow(
+ 'booking_rescheduled',
+ 'After Booking Rescheduled',
+ 'customer_email',
+ 'Customer Email',
+ 'Booking Rescheduled: {%meeting_title%}',
+ '<p>Hi {%customer_name%},</p><p>Your booking has been rescheduled.</p>'
+ . '<table style="border-collapse:collapse;width:100%;margin:20px 0;">'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;width:40%;">Meeting</td><td style="padding:8px 0;color:#556880;">{%meeting_title%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">New Date</td><td style="padding:8px 0;color:#556880;">{%meeting_date%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">New Time</td><td style="padding:8px 0;color:#556880;">{%meeting_time%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Location</td><td style="padding:8px 0;color:#556880;">{%meeting_location%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Host</td><td style="padding:8px 0;color:#556880;">{%host_name%}</td></tr>'
+ . '</table><p>Thank you!</p>'
+ ),
+ ),
+
+ array(
+ 'name' => 'Booking Rescheduled – Host',
+ 'trigger' => 'booking_rescheduled',
+ 'flow_config' => self::simple_flow(
+ 'booking_rescheduled',
+ 'After Booking Rescheduled',
+ 'host_email',
+ 'Host Email',
+ 'Booking Rescheduled: {%meeting_title%}',
+ '<p>Hi {%host_name%},</p><p>A booking has been rescheduled.</p>'
+ . '<table style="border-collapse:collapse;width:100%;margin:20px 0;">'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;width:40%;">Meeting</td><td style="padding:8px 0;color:#556880;">{%meeting_title%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">New Date</td><td style="padding:8px 0;color:#556880;">{%meeting_date%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">New Time</td><td style="padding:8px 0;color:#556880;">{%meeting_time%}</td></tr>'
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Customer</td><td style="padding:8px 0;color:#556880;">{%customer_name%} ({%customer_email%})</td></tr>'
+ . '</table><p>Thank you!</p>'
+ ),
+ ),
+
+ // ── Booking Reminder (24 h before) ─────────────────────────────
+ array(
+ 'name' => 'Booking Reminder – Customer',
+ 'trigger' => 'booking_created',
+ 'flow_config' => self::reminder_flow(
+ 'booking_created',
+ 'After Booking Confirmation',
+ 'customer_email',
+ 'Customer Email',
+ 'Reminder: {%meeting_title%} is coming up',
+ '<p>Hi {%customer_name%},</p><p>This is a reminder that your meeting is coming up soon.</p>'
+ . $booking_tags
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Host</td><td style="padding:8px 0;color:#556880;">{%host_name%}</td></tr>'
+ . '</table><p>We look forward to seeing you!</p><p>Thank you!</p>'
+ ),
+ ),
+
+ array(
+ 'name' => 'Booking Reminder – Host',
+ 'trigger' => 'booking_created',
+ 'flow_config' => self::reminder_flow(
+ 'booking_created',
+ 'After Booking Confirmation',
+ 'host_email',
+ 'Host Email',
+ 'Reminder: {%meeting_title%} is coming up',
+ '<p>Hi {%host_name%},</p><p>This is a reminder that a meeting is coming up soon.</p>'
+ . $booking_tags
+ . '<tr><td style="padding:8px 0;font-weight:600;color:#0C274A;">Customer</td><td style="padding:8px 0;color:#556880;">{%customer_name%} ({%customer_email%})</td></tr>'
+ . '</table><p>Thank you!</p>'
+ ),
+ ),
+
+ );
+ }
+}
--- a/timetics/core/admin/notification.php
+++ b/timetics/core/admin/notification.php
@@ -0,0 +1,301 @@
+<?php
+/**
+ * Notification class — boots themewinter/email-notification-sdk and registers
+ * Timetics triggers so the React automation builder can wire up email flows.
+ *
+ * @package Timetics
+ */
+
+namespace TimeticsCoreAdmin;
+
+use EnsCoreSDK;
+use TimeticsCoreAppointmentsAppointment;
+use TimeticsCoreBookingsBooking;
+use TimeticsCoreCustomersCustomer;
+use TimeticsCoreStaffsStaff;
+use TimeticsUtilsSingleton;
+
+defined( 'ABSPATH' ) || exit;
+
+require_once __DIR__ . '/notification-seeder.php';
+
+class Notification {
+
+ use Singleton;
+
+ /**
+ * Boot SDK and register trigger definitions.
+ *
+ * @return void
+ */
+ public function init() {
+ if ( ! class_exists( SDK::class ) ) {
+ return;
+ }
+
+ SDK::get_instance()
+ ->setup(
+ array(
+ 'plugin_name' => 'Timetics',
+ 'plugin_slug' => 'timetics',
+ 'general_prefix' => 'tt',
+ 'hook_prefix' => 'timetics',
+ 'text_domain' => 'timetics',
+ 'admin_script_handler' => 'timetics-dashboard-scripts',
+ 'sub_menu_filter_hook' => 'timetics_menu',
+ 'sub_menu_details' => array(
+ 'id' => 'timetics-automation',
+ 'title' => __( 'Automation', 'timetics' ),
+ 'link' => '/automation',
+ 'capability' => apply_filters( 'timetics_menu_permission_automation', 'manage_options' ),
+ 'position' => apply_filters( 'timetics_menu_position_automation', 11 ),
+ ),
+ )
+ )
+ ->init();
+
+ add_filter( 'ens_tt_available_actions', array( $this, 'register_triggers' ) );
+ add_filter( 'timetics_notification_sdk_email_body', array( $this, 'wrap_email_body' ), 10, 1 );
+ add_filter( 'timetics_notification_sdk_to_emails', array( $this, 'expand_custom_email' ), 10, 2 );
+
+ add_action( 'admin_init', array( 'TimeticsCoreAdminNotification_Seeder', 'maybe_seed' ), 20 );
+ }
+
+ /**
+ * Register the 3 booking triggers with their tag fields and receivers.
+ *
+ * @param array $actions
+ * @return array
+ */
+ public function register_triggers( $actions ) {
+ $trigger_data = array(
+ array(
+ 'label' => __( 'Meeting Title', 'timetics' ),
+ 'value' => 'meeting_title',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Meeting Date', 'timetics' ),
+ 'value' => 'meeting_date',
+ 'type' => 'date',
+ ),
+ array(
+ 'label' => __( 'Meeting Time', 'timetics' ),
+ 'value' => 'meeting_time',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Meeting Location', 'timetics' ),
+ 'value' => 'meeting_location',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Google Meet Link', 'timetics' ),
+ 'value' => 'meeting_meet_link',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Meeting Duration', 'timetics' ),
+ 'value' => 'meeting_duration',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Host Name', 'timetics' ),
+ 'value' => 'host_name',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Host Email', 'timetics' ),
+ 'value' => 'host_email',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Customer Name', 'timetics' ),
+ 'value' => 'customer_name',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Customer Email', 'timetics' ),
+ 'value' => 'customer_email',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Login URL', 'timetics' ),
+ 'value' => 'login_url',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Login Username', 'timetics' ),
+ 'value' => 'login_username',
+ 'type' => 'string',
+ ),
+ array(
+ 'label' => __( 'Set Password URL', 'timetics' ),
+ 'value' => 'set_password_url',
+ 'type' => 'string',
+ ),
+ );
+
+ $email_receivers = array(
+ array(
+ 'label' => __( 'Customer Email', 'timetics' ),
+ 'value' => 'customer_email',
+ ),
+ array(
+ 'label' => __( 'Host Email', 'timetics' ),
+ 'value' => 'host_email',
+ ),
+ array(
+ 'label' => __( 'Custom Email', 'timetics' ),
+ 'value' => 'custom_email',
+ ),
+ );
+
+ $actions = array(
+ array(
+ 'trigger_label' => __( 'After Booking Confirmation', 'timetics' ),
+ 'trigger_value' => 'booking_created',
+ 'trigger_data' => $trigger_data,
+ 'delay_dependencies' => array(
+ array(
+ 'label' => __( 'Meeting Date', 'timetics' ),
+ 'value' => 'meeting_date',
+ ),
+ ),
+ 'email_receivers' => $email_receivers,
+ ),
+ array(
+ 'trigger_label' => __( 'After Booking Cancellation', 'timetics' ),
+ 'trigger_value' => 'booking_canceled',
+ 'trigger_data' => $trigger_data,
+ 'delay_dependencies' => array(),
+ 'email_receivers' => $email_receivers,
+ ),
+ array(
+ 'trigger_label' => __( 'After Booking Rescheduled', 'timetics' ),
+ 'trigger_value' => 'booking_rescheduled',
+ 'trigger_data' => $trigger_data,
+ 'delay_dependencies' => array(),
+ 'email_receivers' => $email_receivers,
+ ),
+ );
+
+ return $actions;
+ }
+
+ /**
+ * Expand a comma-separated custom_email setting into an array of addresses.
+ *
+ * Only expands when the email value matches hook_data['custom_email'],
+ * so customer/host emails pass through untouched.
+ *
+ * Hooked to `timetics_notification_sdk_to_emails`.
+ *
+ * @param string $email Raw email value being sent.
+ * @param array $hook_data Full hook data payload.
+ * @return string|array Single email or array of emails.
+ */
+ public function expand_custom_email( $email, $hook_data ) {
+ if ( ! isset( $hook_data['custom_email'] ) || $email !== $hook_data['custom_email'] ) {
+ return $email;
+ }
+
+ if ( strpos( $email, ',' ) === false ) {
+ return sanitize_email( $email );
+ }
+
+ return array_values(
+ array_filter(
+ array_map( 'sanitize_email', array_map( 'trim', explode( ',', $email ) ) )
+ )
+ );
+ }
+
+ /**
+ * Wrap the SDK email body in Timetics' branded HTML template.
+ *
+ * Hooked to `notification_sdk_email_body`.
+ *
+ * @param string $message Raw email body HTML from the SDK.
+ * @return string Full HTML email.
+ */
+ public function wrap_email_body( $message ) {
+ $template_path = TIMETICS_PLUGIN_DIR . '/templates/emails/email-notification-wrapper.html';
+
+ if ( ! file_exists( $template_path ) ) {
+ return $message;
+ }
+
+ $template = file_get_contents( $template_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ $primary_color = timetics_get_option( 'primary_color', '#3161F1' );
+ $company_name = timetics_get_option( 'business_name', get_bloginfo( 'name' ) );
+
+ $show_powered_by = apply_filters( 'timetics_show_email_powered_by', true );
+ $powered_by_html = '';
+ if ( $show_powered_by ) {
+ $powered_by_html = '<p style="margin:8px 0 0;font-size:11px;color:#b0bec9;font-style:italic;">'
+ . esc_html__( 'Powered by Timetics', 'timetics' )
+ . '</p>';
+ }
+
+ $variables = array(
+ '{{MESSAGE}}' => wp_kses_post( $message ),
+ '{{COMPANY_NAME}}' => esc_html( $company_name ),
+ '{{PRIMARY_COLOR}}' => esc_attr( $primary_color ),
+ '{%powered_by_section%}' => $powered_by_html,
+ );
+
+ return str_replace( array_keys( $variables ), array_values( $variables ), $template );
+ }
+
+ /**
+ * Build the hook data payload from a Booking object.
+ * Called from any file that needs to fire a timetics_gln_hook action.
+ *
+ * @param Booking $booking
+ * @return array
+ */
+ public static function get_hook_data( Booking $booking ) {
+ $meeting = new Appointment( $booking->get_appointment() );
+ $staff = new Staff( $booking->get_staff_id() );
+ $customer = new Customer( $booking->get_customer_id() );
+
+ $formatted = timetics_format_email_datetime( $booking->get_start_date(), $booking->get_start_time() );
+ $booking_timestamp = strtotime( $booking->get_start_date() . ' ' . $booking->get_start_time() );
+
+ /* Pull the Google Meet link from the stored calendar event so it can be inserted as the {%meeting_meet_link%} tag in automation emails.
+ */
+ $calendar_event = $booking->get_event();
+ $meet_link = ( is_array( $calendar_event ) && ! empty( $calendar_event['hangoutLink'] ) ) ? $calendar_event['hangoutLink'] : '';
+
+ $set_password_url = '';
+ $customer_user = get_user_by( 'email', $customer->get_email() );
+ if ( $customer_user ) {
+ $reset_key = get_password_reset_key( $customer_user );
+ if ( ! is_wp_error( $reset_key ) ) {
+ $set_password_url = network_site_url(
+ 'wp-login.php?action=rp&key=' . $reset_key . '&login=' . rawurlencode( $customer_user->user_login ),
+ 'login'
+ );
+ }
+ }
+
+ return array(
+ 'customer_email' => $customer->get_email(),
+ 'host_email' => $staff->get_email(),
+ 'custom_email' => apply_filters( 'timetics_custom_notification_email', timetics_get_option( 'custom_notification_email', get_option( 'admin_email' ) ) ),
+ 'meeting_title' => $meeting->get_name(),
+ 'meeting_date' => $formatted['date'],
+ 'meeting_date_timestamp' => $booking_timestamp,
+ 'meeting_time' => $formatted['time'],
+ 'meeting_location' => $booking->get_location(),
+ 'meeting_meet_link' => $meet_link,
+ 'meeting_duration' => $meeting->get_duration(),
+ 'host_name' => $staff->get_display_name(),
+ 'customer_name' => $customer->get_display_name(),
+ 'login_url' => wp_login_url(),
+ 'login_username' => $customer->get_email(),
+ 'set_password_url' => $set_password_url,
+ );
+ }
+}
--- a/timetics/core/base.php
+++ b/timetics/core/base.php
@@ -61,6 +61,7 @@
Google_Calendar_Sync::instance();
Api_Booking_Calendar::instance();
Api_Addon::instance();
+ add_action( 'init', array( AdminNotification::instance(), 'init' ), 15 );
}
}
--- a/timetics/core/bookings/api-booking.php
+++ b/timetics/core/bookings/api-booking.php
@@ -11,6 +11,7 @@
use TimeticsCoreAppointmentsApi_Appointment;
use TimeticsCoreAppointmentsAppointment;
use TimeticsCoreCustomersCustomer;
+use TimeticsCoreAdminNotification;
use TimeticsCoreEmailsCancel_Event_Customer_Email;
use TimeticsCoreEmailsCancel_Event_Email;
use TimeticsCoreEmailsNew_Event_Customer_Email;
@@ -788,6 +789,8 @@
$new_event_customer_email->send();
}
+ do_action( 'timetics_gln_hook', 'booking_created', Notification::get_hook_data( $booking ) );
+
do_action( 'timetics_booking_payment', $booking );
}
@@ -1067,24 +1070,10 @@
// Fire when booking is completed.
do_action( 'timetics_after_booking_create', $booking->get_id(), $customer->get_id(), $meeting->get_id(), $data );
- // Send booking creation emails for new bookings not processed through
- // a separate payment flow. Online gateways (stripe/paypal/woocommerce)
- // send this email themselves once payment is finalized, so excluding
- // them here avoids a duplicate email for the same booking.
- if ( 'created' === $action && 'failed' !== $status && ! in_array( $payment_method_l, ['stripe', 'paypal', 'woocommerce'], true ) ) {
- $is_email_to_customer = timetics_get_option( 'booking_created_customer');
- $is_email_to_host = timetics_get_option( 'booking_created_host');
-
- if ( $is_email_to_host ) {
- $new_event_email = new New_Event_Email( $booking );
- $new_event_email->send();
- }
-
- if ( $is_email_to_customer ) {
- $new_event_customer_email = new New_Event_Customer_Email( $booking );
- $new_event_customer_email->send();
- }
- }
+ // Note: booking creation emails for new bookings are sent further below,
+ // AFTER the calendar event is created, so the Google Meet join link is
+ // available in the email. See the "created" branch after the schedule
+ // entry is created.
// Create or update calendar event.
if ( $id ) {
@@ -1103,6 +1092,8 @@
$customer_cancel_event_email->send();
}
+ do_action( 'timetics_gln_hook', 'booking_canceled', Notification::get_hook_data( $booking ) );
+
/**
* Added temporary for leagacy sass. It will remove in future.
*/
@@ -1130,6 +1121,8 @@
$update_event_customer_email = new Update_Event_Customer_Email( $booking );
$update_event_customer_email->send();
}
+
+ do_action( 'timetics_gln_hook', 'booking_rescheduled', Notification::get_hook_data( $booking ) );
}
}
}
@@ -1181,6 +1174,36 @@
$booking_entry->create( $book_entry_data );
}
+ // For newly created bookings, create the calendar event now that the
+ // booking schedule entry exists. This generates the Google Meet link
+ // (stored in booking meta) so it can be shown on the success page and
+ // included in the notification emails sent below.
+ if ( 'created' === $action && 'cancel' !== $status ) {
+ $booking->create_event();
+ }
+
+ // Send booking creation emails for new bookings not processed through
+ // a separate payment flow. Online gateways (stripe/paypal/woocommerce)
+ // send this email themselves once payment is finalized, so excluding
+ // them here avoids a duplicate email for the same booking. Sent here
+ // (after create_event) so the Google Meet link is present in the email.
+ if ( 'created' === $action && 'failed' !== $status && ! in_array( $payment_method_l, ['stripe', 'paypal', 'woocommerce'], true ) ) {
+ $is_email_to_customer = timetics_get_option( 'booking_created_customer');
+ $is_email_to_host = timetics_get_option( 'booking_created_host');
+
+ if ( $is_email_to_host ) {
+ $new_event_email = new New_Event_Email( $booking );
+ $new_event_email->send();
+ }
+
+ if ( $is_email_to_customer ) {
+ $new_event_customer_email = new New_Event_Customer_Email( $booking );
+ $new_event_customer_email->send();
+ }
+
+ do_action( 'timetics_gln_hook', 'booking_created', Notification::get_hook_data( $booking ) );
+ }
+
// Fire after booking schedule create.
do_action( 'timetics_after_booking_schedule', $booking->get_id(), $customer->get_id(), $meeting->get_id(), $data );
@@ -1355,6 +1378,8 @@
$customer_cancel_event_email->send();
}
+ do_action( 'timetics_gln_hook', 'booking_canceled', Notification::get_hook_data( $booking ) );
+
do_action( 'timetics_after_booking_delete', $recurrences );
--- a/timetics/core/bookings/booking.php
+++ b/timetics/core/bookings/booking.php
@@ -801,6 +801,8 @@
]
);
+ $event = false;
+
if ( $entries ) {
$entry = $booking_entry->first();
--- a/timetics/core/integrations/google/service/calendar.php
+++ b/timetics/core/integrations/google/service/calendar.php
@@ -364,9 +364,14 @@
$args['end'] = [$date['end']];
if ( $args['google_meet'] ) {
+ // requestId must be unique per request; Google ignores the
+ // conference create request (no Meet link generated) if it
+ // matches a previously used id.
+ $request_id = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'tt-meet-', true );
+
$args['conferenceData'] = [
'createRequest' => [
- 'requestId' => 'sample123',
+ 'requestId' => $request_id,
'conferenceSolutionKey' => ['type' => 'hangoutsMeet'],
],
];
--- a/timetics/core/integrations/woocommerce/hooks.php
+++ b/timetics/core/integrations/woocommerce/hooks.php
@@ -8,6 +8,7 @@
defined( 'ABSPATH' ) || exit;
+use TimeticsCoreAdminNotification;
use TimeticsCoreAppointmentsAppointment;
use TimeticsCoreBookingsBooking;
use TimeticsCoreCustomersCustomer;
@@ -158,6 +159,8 @@
$new_event_customer_email->send();
}
+ do_action( 'timetics_gln_hook', 'booking_created', Notification::get_hook_data( $booking ) );
+
WC()->session->set( 'timetics_data', null );
}
@@ -714,6 +717,8 @@
$customer_cancel_event_email = new Cancel_Event_Customer_Email( $booking );
$customer_cancel_event_email->send();
}
+
+ do_action( 'timetics_gln_hook', 'booking_canceled', Notification::get_hook_data( $booking ) );
}
} finally {
--- a/timetics/templates/emails/cancel-event.php
+++ b/timetics/templates/emails/cancel-event.php
@@ -81,7 +81,7 @@
printf( esc_html__( '%1$s, %2$s %3$s', 'timetics' ), esc_html( $timetics_time ), esc_html( $timetics_day ), esc_html( $timetics_date ), esc_html( $timetics_timezone ) );?></p>
</div>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
--- a/timetics/templates/emails/customer-booking-reminder.php
+++ b/timetics/templates/emails/customer-booking-reminder.php
@@ -48,7 +48,7 @@
</div>
<?php if ( $timetics_email_body ): ?>
<?php echo wp_kses( timetics_replace_placeholder( $timetics_email_body, $timetics_placeholders ), 'post' ); ?>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
--- a/timetics/templates/emails/customer-cancel-event.php
+++ b/timetics/templates/emails/customer-cancel-event.php
@@ -82,7 +82,7 @@
printf( esc_html__( '%1$s, %2$s %3$s', 'timetics' ), esc_html( $timetics_time ), esc_html( $timetics_day ), esc_html( $timetics_date ), esc_html( $timetics_timezone ) );?></p>
</div>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
--- a/timetics/templates/emails/customer-update-event.php
+++ b/timetics/templates/emails/customer-update-event.php
@@ -50,7 +50,7 @@
<?php if ( $timetics_email_body ): ?>
<?php echo wp_kses( timetics_replace_placeholder( $timetics_email_body, $timetics_placeholders ), 'post' ); ?>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
@@ -95,7 +95,7 @@
printf( esc_html__( '%1$s, %2$s %3$s', 'timetics' ), esc_html( $timetics_time ), esc_html( $timetics_day ), esc_html( $timetics_date ), esc_html( $timetics_timezone ) );?></p>
</div>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
--- a/timetics/templates/emails/staff-booking-reminder.php
+++ b/timetics/templates/emails/staff-booking-reminder.php
@@ -48,7 +48,7 @@
</div>
<?php if ( $timetics_email_body ): ?>
<?php echo wp_kses( timetics_replace_placeholder( $timetics_email_body, $timetics_placeholders ), 'post' ); ?>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
--- a/timetics/templates/emails/update-event.php
+++ b/timetics/templates/emails/update-event.php
@@ -49,7 +49,7 @@
<?php if ( $timetics_email_body ): ?>
<?php echo wp_kses( timetics_replace_placeholder( $timetics_email_body, $timetics_placeholders ), 'post' ); ?>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
@@ -94,7 +94,7 @@
printf( esc_html__( '%1$s, %2$s %3$s', 'timetics' ), esc_html( $timetics_time ), esc_html( $timetics_day ), esc_html( $timetics_date ), esc_html( $timetics_timezone ) );?></p>
</div>
- <?php if ( 'virtual' === $timetics_location_type && $timetics_join_link ): ?>
+ <?php if ( 'google-meet' === $timetics_location_type && $timetics_join_link ): ?>
<div class="single-data-entry" style="margin: 10px 0 20px;">
<p style="font-weight: 600; font-size: 14px; line-height: 1; color: #0C274A; margin: 0 0 5px;">
<?php esc_html_e( 'Location:', 'timetics' );?>
--- a/timetics/timetics.php
+++ b/timetics/timetics.php
@@ -4,7 +4,7 @@
* Plugin Name: Timetics - Appointment Booking Calendar & Scheduling System
* Plugin URI: https://arraytics.com/timetics/
* Description: Schedule, Appointment and Seat Booking plugin.
- * Version: 1.0.58
+ * Version: 1.0.59
* Requires at least: 5.2
* Requires PHP: 7.3
* Author: Arraytics
--- a/timetics/uninstall.php
+++ b/timetics/uninstall.php
@@ -8,3 +8,4 @@
delete_option( 'timetics_onboard_setup' );
delete_option( 'timetics_onboard_settings' );
+delete_option( 'timetics_default_flows_seeded' );
--- a/timetics/vendor/arraytics/tools-sdk/src/PluginManager.php
+++ b/timetics/vendor/arraytics/tools-sdk/src/PluginManager.php
@@ -26,8 +26,8 @@
$plugins = get_plugins();
if ( is_array( $plugins ) ) {
- foreach( $plugins as $plugin ) {
- if ( $plugin['TextDomain'] === $slug ) {
+ foreach( $plugins as $plugin_path => $plugin ) {
+ if ( strpos( $plugin_path, $slug . '/' ) === 0 ) {
return true;
}
}
@@ -122,7 +122,7 @@
if ( is_array( $plugins ) ) {
foreach( $plugins as $plugin_path => $plugin ) {
- if ( $plugin['TextDomain'] === $slug ) {
+ if ( strpos( $plugin_path, $slug . '/' ) === 0 ) {
return $plugin_path;
}
}
--- a/timetics/vendor/autoload.php
+++ b/timetics/vendor/autoload.php
@@ -19,4 +19,4 @@
require_once __DIR__ . '/composer/autoload_real.php';
-return ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9::getLoader();
+return ComposerAutoloaderInitb20117cddd741a51e3bb6f90a34768e1::getLoader();
--- a/timetics/vendor/composer/autoload_files.php
+++ b/timetics/vendor/composer/autoload_files.php
@@ -0,0 +1,10 @@
+<?php
+
+// autoload_files.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+ '65bd208c04f25e98cf12b5c37b014f1e' => $vendorDir . '/themewinter/email-notification-sdk/src/Utils/global-helpers.php',
+);
--- a/timetics/vendor/composer/autoload_psr4.php
+++ b/timetics/vendor/composer/autoload_psr4.php
@@ -7,5 +7,6 @@
return array(
'UninstallerForm\' => array($vendorDir . '/themewinter/uninstaller_form/src'),
+ 'Ens\' => array($vendorDir . '/themewinter/email-notification-sdk/src'),
'Arraytics\ToolsSdk\' => array($vendorDir . '/arraytics/tools-sdk/src'),
);
--- a/timetics/vendor/composer/autoload_real.php
+++ b/timetics/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer
-class ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9
+class ComposerAutoloaderInitb20117cddd741a51e3bb6f90a34768e1
{
private static $loader;
@@ -22,15 +22,27 @@
return self::$loader;
}
- spl_autoload_register(array('ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9', 'loadClassLoader'), true, true);
+ spl_autoload_register(array('ComposerAutoloaderInitb20117cddd741a51e3bb6f90a34768e1', 'loadClassLoader'), true, true);
self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
- spl_autoload_unregister(array('ComposerAutoloaderInitd02707597fbf5927c7666b74bdb40ea9', 'loadClassLoader'));
+ spl_autoload_unregister(array('ComposerAutoloaderInitb20117cddd741a51e3bb6f90a34768e1', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
- call_user_func(ComposerAutoloadComposerStaticInitd02707597fbf5927c7666b74bdb40ea9::getInitializer($loader));
+ call_user_func(ComposerAutoloadComposerStaticInitb20117cddd741a51e3bb6f90a34768e1::getInitializer($loader));
$loader->register(true);
+ $filesToLoad = ComposerAutoloadComposerStaticInitb20117cddd741a51e3bb6f90a34768e1::$files;
+ $requireFile = Closure::bind(static function ($fileIdentifier, $file) {
+ if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+ $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+ require $file;
+ }
+ }, null, null);
+ foreach ($filesToLoad as $fileIdentifier => $file) {
+ $requireFile($fileIdentifier, $file);
+ }
+
return $loader;
}
}
--- a/timetics/vendor/composer/autoload_static.php
+++ b/timetics/vendor/composer/autoload_static.php
@@ -4,13 +4,21 @@
namespace ComposerAutoload;
-class ComposerStaticInitd02707597fbf5927c7666b74bdb40ea9
+class ComposerStaticInitb20117cddd741a51e3bb6f90a34768e1
{
+ public static $files = array (
+ '65bd208c04f25e98cf12b5c37b014f1e' => __DIR__ . '/..' . '/themewinter/email-notification-sdk/src/Utils/global-helpers.php',
+ );
+
public static $prefixLengthsPsr4 = array (
'U' =>
array (
'UninstallerForm\' => 16,
),
+ 'E' =>
+ array (
+ 'Ens\' => 4,
+ ),
'A' =>
array (
'Arraytics\ToolsSdk\' => 19,
@@ -22,6 +30,10 @@
array (
0 => __DIR__ . '/..' . '/themewinter/uninstaller_form/src',
),
+ 'Ens\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/themewinter/email-notification-sdk/src',
+ ),
'Arraytics\ToolsSdk\' =>
array (
0 => __DIR__ . '/..' . '/arraytics/tools-sdk/src',
@@ -35,9 +47,9 @@
public static function getInitializer(ClassLoader $loader)
{
return Closure::bind(function () use ($loader) {
- $loader->prefixLengthsPsr4 = ComposerStaticInitd02707597fbf5927c7666b74bdb40ea9::$prefixLengthsPsr4;
- $loader->prefixDirsPsr4 = ComposerStaticInitd02707597fbf5927c7666b74bdb40ea9::$prefixDirsPsr4;
- $loader->classMap = ComposerStaticInitd02707597fbf5927c7666b74bdb40ea9::$classMap;
+ $loader->prefixLengthsPsr4 = ComposerStaticInitb20117cddd741a51e3bb6f90a34768e1::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInitb20117cddd741a51e3bb6f90a34768e1::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInitb20117cddd741a51e3bb6f90a34768e1::$classMap;
}, null, ClassLoader::class);
}
--- a/timetics/vendor/composer/installed.php
+++ b/timetics/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'arraytics/timetics',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
- 'reference' => '62593009d43fc6b91338a808dc0a704eebb327fb',
+ 'reference' => 'a0a14248bb2af91841425d1c2b5ff294fd813b32',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'arraytics/timetics' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
- 'reference' => '62593009d43fc6b91338a808dc0a704eebb327fb',
+ 'reference' => 'a0a14248bb2af91841425d1c2b5ff294fd813b32',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -28,6 +28,17 @@
'aliases' => array(
0 => '9999999-dev',
),
+ 'dev_requirement' => false,
+ ),
+ 'themewinter/email-notification-sdk' => array(
+ 'pretty_version' => 'dev-main',
+ 'version' => 'dev-main',
+ 'reference' => 'cd56595844787809949d2e838c636064c6436807',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../themewinter/email-notification-sdk',
+ 'aliases' => array(
+ 0 => '9999999-dev',
+ ),
'dev_requirement' => false,
),
'themewinter/uninstaller_form' => array(