Changeset 3450989
- Timestamp:
- 01/31/2026 12:29:07 PM (3 weeks ago)
- Location:
- sync-engine-for-intercom/trunk
- Files:
-
- 7 edited
-
README.txt (modified) (5 diffs)
-
includes/api/class-intercom-contacts.php (modified) (1 diff)
-
includes/api/class-intercom-tags.php (modified) (6 diffs)
-
includes/core/class-action-scheduler-handler.php (modified) (8 diffs)
-
includes/sync/class-event-sync.php (modified) (2 diffs)
-
includes/sync/class-user-sync.php (modified) (1 diff)
-
sync-engine-for-intercom.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
sync-engine-for-intercom/trunk/README.txt
r3420647 r3450989 7 7 WC requires at least: 8.0 8 8 WC tested up to: 10.4 9 Stable tag: 1.0.39 Stable tag: 2.0.1 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 20 20 This plugin is ideal for SaaS, membership sites, and WooCommerce stores that want accurate Intercom user data without custom development or expensive automation tools. 21 21 22 = Demo Video = 23 24 See Sync Engine in action—connect WordPress and WooCommerce to Intercom, sync users, events, and tags. 25 26 [youtube https://www.youtube.com/watch?v=Gz2w-_S4aeA] 27 22 28 = Intercom Integration for WordPress = 23 29 24 Sync Engine is a complete Intercom integrationfor WordPress that seamlessly connects your site with Intercom. It automatically syncs WordPress users, updates Intercom profiles when data changes, and sends WordPress events, tags, and metadata to Intercom.30 Sync Engine is a complete [WordPress Intercom Integration](https://ripplestep.com/wordpress-intercom-integration/) for WordPress that seamlessly connects your site with Intercom. It automatically syncs WordPress users, updates Intercom profiles when data changes, and sends WordPress events, tags, and metadata to Intercom. 25 31 26 32 Whether you're running a membership site, LMS platform, WooCommerce store, or any WordPress site, this plugin provides a robust Intercom API WordPress integration that keeps your customer data synchronized in real-time. … … 33 39 * **Real-Time Event Tracking**: Send WordPress events to Intercom automatically. Track user registrations, logins, profile updates, WooCommerce orders, cart additions, and custom events. All events are synced to Intercom for powerful segmentation and automation. 34 40 35 * **WooCommerce Integration**: Full WooCommerce Intercom integrationsupport. Track order events, cart activity, customer lifetime value, and purchase behavior. Sync WooCommerce customer data to Intercom custom attributes.41 * **WooCommerce Integration**: Full [WooCommerce Intercom Integration](https://ripplestep.com/intercom-woocommerce/) support. Track order events, cart activity, customer lifetime value, and purchase behavior. Sync WooCommerce customer data to Intercom custom attributes. 36 42 37 43 * **Custom Events Support**: Track any custom WordPress event and send it to Intercom. Use the simple API to track custom actions, milestones, or any user activity you want to monitor in Intercom. … … 207 213 = Is this plugin free? = 208 214 209 Yes, Sync Engine is free to use. Advanced features are offered in premium version. 215 Yes, Sync Engine is free to use. Advanced features are offered in premium version. For support or questions, use our [contact form](https://ripplestep.com/contact/). 210 216 211 217 = Does this work with WooCommerce? = 212 218 213 Yes, this plugin includes full WooCommerce Intercom integration. It syncs WooCommerce customers to Intercom, tracks order events, monitors cart activity, and sends purchase data to Intercom for segmentation and automation.219 Yes, this plugin includes full [WooCommerce Intercom Integration](https://ripplestep.com/intercom-woocommerce/). It syncs WooCommerce customers to Intercom, tracks order events, monitors cart activity, and sends purchase data to Intercom for segmentation and automation. 214 220 215 221 = How do I sync custom WordPress user metadata to Intercom? = … … 258 264 259 265 = Changelog = 266 267 = 2.0.1 = 268 * Async Role Tag Added 269 * HPOS Compatiblity 270 * Reduced API Calls for User Sync And Event Sync 271 * Used Rate Limiting along With Instant Async Call 272 260 273 261 274 = 1.0.3 = -
sync-engine-for-intercom/trunk/includes/api/class-intercom-contacts.php
r3420647 r3450989 152 152 return false; 153 153 } 154 } 155 156 /** 157 * Get contact ID with caching to minimize API calls. 158 * 159 * This method: 160 * 1. Checks WordPress user meta for cached contact_id 161 * 2. If not cached, searches Intercom by external_id or email 162 * 3. Caches the contact_id in user meta for future lookups 163 * 164 * @since 1.0.0 165 * @param int $user_id WordPress user ID. 166 * @param string|null $external_id Optional external ID (e.g., 'wp_user_123'). 167 * @param string|null $email Optional email address. 168 * @param bool $force_refresh Force refresh from Intercom (default: false). 169 * @return string|false Contact ID or false if not found. 170 */ 171 public function get_cached_contact_id( int $user_id, ?string $external_id = null, ?string $email = null, bool $force_refresh = false ): string|false { 172 $prefix = 'rpplstp_iws'; 173 174 // Check cache first (unless force refresh). 175 if ( ! $force_refresh ) { 176 $cached_contact_id = get_user_meta( $user_id, $prefix . '_intercom_contact_id', true ); 177 if ( ! empty( $cached_contact_id ) ) { 178 $this->logger->debug( 'Using cached contact_id', array( 179 'user_id' => $user_id, 180 'contact_id' => $cached_contact_id, 181 ) ); 182 return $cached_contact_id; 183 } 184 } 185 186 // Cache miss or force refresh - lookup from Intercom. 187 $contact_id = null; 188 189 // Try external_id first (most reliable). 190 if ( ! empty( $external_id ) ) { 191 $contact_id = $this->find_by_external_id( $external_id ); 192 if ( $contact_id ) { 193 $this->logger->debug( 'Contact found by external_id', array( 194 'user_id' => $user_id, 195 'external_id' => $external_id, 196 'contact_id' => $contact_id, 197 ) ); 198 } 199 } 200 201 // Fallback to email search. 202 if ( ! $contact_id && ! empty( $email ) ) { 203 $contact_id = $this->find_by_email( $email ); 204 if ( $contact_id ) { 205 $this->logger->debug( 'Contact found by email', array( 206 'user_id' => $user_id, 207 'email' => $email, 208 'contact_id' => $contact_id, 209 ) ); 210 } 211 } 212 213 // Cache the result (even if false, to avoid repeated lookups for non-existent contacts). 214 if ( $contact_id ) { 215 update_user_meta( $user_id, $prefix . '_intercom_contact_id', $contact_id ); 216 $this->logger->debug( 'Cached contact_id in user meta', array( 217 'user_id' => $user_id, 218 'contact_id' => $contact_id, 219 ) ); 220 } 221 222 return $contact_id ?: false; 223 } 224 225 /** 226 * Clear cached contact ID for a user. 227 * 228 * Call this when user email changes or contact needs to be re-synced. 229 * 230 * @since 1.0.0 231 * @param int $user_id WordPress user ID. 232 * @return bool True on success, false on failure. 233 */ 234 public function clear_cached_contact_id( int $user_id ): bool { 235 $prefix = 'rpplstp_iws'; 236 return delete_user_meta( $user_id, $prefix . '_intercom_contact_id' ); 154 237 } 155 238 -
sync-engine-for-intercom/trunk/includes/api/class-intercom-tags.php
r3420647 r3450989 21 21 use Intercom\Tags\Types\Tag; 22 22 use Intercom\Types\TagList; 23 use Intercom\Contacts\Requests\ListTagsAttachedToContactRequest; 23 24 24 25 /** … … 116 117 $tag_list = $this->api->api_call( 'tags', 'list' ); 117 118 119 // Debug: Log raw response type and structure. 120 $this->logger->debug( 'Raw tag list response', array( 121 'type' => gettype( $tag_list ), 122 'class' => is_object( $tag_list ) ? get_class( $tag_list ) : 'not_object', 123 'has_getTags' => is_object( $tag_list ) && method_exists( $tag_list, 'getTags' ), 124 'has_tags_prop' => is_object( $tag_list ) && isset( $tag_list->tags ), 125 'has_getData' => is_object( $tag_list ) && method_exists( $tag_list, 'getData' ), 126 'is_array' => is_array( $tag_list ), 127 ) ); 128 118 129 if ( ! $tag_list ) { 130 $this->logger->warning( 'Tag list returned empty', array( 'searching_for' => $tag_name ) ); 119 131 return false; 120 132 } 121 133 122 // Get tags from TagList .134 // Get tags from TagList - try multiple access patterns. 123 135 $tags = array(); 136 137 // Pattern 1: getTags() method 124 138 if ( method_exists( $tag_list, 'getTags' ) ) { 125 139 $tags = $tag_list->getTags(); 126 } elseif ( isset( $tag_list->tags ) ) { 140 $this->logger->debug( 'Using getTags() method', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) ); 141 } 142 // Pattern 2: getData() method (paginated response) 143 elseif ( method_exists( $tag_list, 'getData' ) ) { 144 $tags = $tag_list->getData(); 145 $this->logger->debug( 'Using getData() method', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) ); 146 } 147 // Pattern 3: tags property 148 elseif ( isset( $tag_list->tags ) ) { 127 149 $tags = $tag_list->tags; 128 } elseif ( is_array( $tag_list ) ) { 150 $this->logger->debug( 'Using tags property', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) ); 151 } 152 // Pattern 4: data property (common in Intercom responses) 153 elseif ( isset( $tag_list->data ) ) { 154 $tags = $tag_list->data; 155 $this->logger->debug( 'Using data property', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) ); 156 } 157 // Pattern 5: Direct array 158 elseif ( is_array( $tag_list ) ) { 129 159 $tags = $tag_list; 130 } 160 $this->logger->debug( 'Using direct array', array( 'count' => count( $tags ) ) ); 161 } 162 163 // Collect all tag names for debugging. 164 $all_tag_names = array(); 165 foreach ( $tags as $tag ) { 166 if ( method_exists( $tag, 'getName' ) ) { 167 $all_tag_names[] = $tag->getName(); 168 } elseif ( isset( $tag->name ) ) { 169 $all_tag_names[] = $tag->name; 170 } elseif ( is_array( $tag ) && isset( $tag['name'] ) ) { 171 $all_tag_names[] = $tag['name']; 172 } 173 } 174 175 $this->logger->debug( 'Searching for tag in Intercom', array( 176 'searching_for' => $tag_name, 177 'total_tags' => count( $all_tag_names ), 178 'all_tags' => $all_tag_names, 179 ) ); 131 180 132 181 // Search for tag by name (case-insensitive). … … 134 183 foreach ( $tags as $tag ) { 135 184 $current_name = ''; 185 $current_id = ''; 186 187 // Extract name 136 188 if ( method_exists( $tag, 'getName' ) ) { 137 189 $current_name = $tag->getName(); 138 190 } elseif ( isset( $tag->name ) ) { 139 191 $current_name = $tag->name; 192 } elseif ( is_array( $tag ) && isset( $tag['name'] ) ) { 193 $current_name = $tag['name']; 194 } 195 196 // Extract ID 197 if ( method_exists( $tag, 'getId' ) ) { 198 $current_id = $tag->getId(); 199 } elseif ( isset( $tag->id ) ) { 200 $current_id = $tag->id; 201 } elseif ( is_array( $tag ) && isset( $tag['id'] ) ) { 202 $current_id = $tag['id']; 140 203 } 141 204 142 205 if ( strtolower( $current_name ) === $tag_name_lower ) { 143 if ( method_exists( $tag, 'getId' ) ) { 144 return $tag->getId(); 145 } elseif ( isset( $tag->id ) ) { 146 return $tag->id; 147 } 148 } 149 } 206 return $current_id ?: false; 207 } 208 } 209 210 $this->logger->warning( 'Tag not found in Intercom', array( 211 'searching_for' => $tag_name, 212 'available_tags' => $all_tag_names, 213 ) ); 150 214 151 215 return false; … … 205 269 } 206 270 207 try { 208 // Find the tag ID. 209 $tag_id = $this->find_tag_by_name( $tag_name ); 210 if ( ! $tag_id ) { 211 // Tag doesn't exist, nothing to untag. 212 return true; 213 } 214 215 // Untag the contact. 216 $request = new UntagContactRequest( array( 217 'contactId' => $contact_id, 218 'tagId' => $tag_id, 271 try { 272 // Find the tag ID. 273 $tag_id = $this->find_tag_by_name( $tag_name ); 274 if ( ! $tag_id ) { 275 // Tag doesn't exist in Intercom, nothing to untag. 276 $this->logger->debug( 'Tag not found in Intercom, skipping untag', array( 277 'contact_id' => $contact_id, 278 'tag_name' => $tag_name, 219 279 ) ); 220 280 221 $this->api->api_call( 'tags', 'untagContact', $request ); 222 $this->logger->debug( 'Contact untagged successfully', array( 'contact_id' => $contact_id, 'tag_name' => $tag_name ) ); 281 223 282 return true; 283 } 284 285 $this->logger->debug( 'Found tag for untagging', array( 286 'tag_name' => $tag_name, 287 'tag_id' => $tag_id, 288 ) ); 289 290 // Untag the contact. 291 $request = new UntagContactRequest( array( 292 'contactId' => $contact_id, 293 'tagId' => $tag_id, 294 ) ); 295 296 $this->api->api_call( 'tags', 'untagContact', $request ); 297 $this->logger->info( 'Contact untagged successfully', array( 298 'contact_id' => $contact_id, 299 'tag_name' => $tag_name, 300 'tag_id' => $tag_id, 301 ) ); 302 return true; 224 303 } catch ( \Exception $e ) { 225 304 $this->logger->error( 'Failed to untag contact', array( 'contact_id' => $contact_id, 'tag_name' => $tag_name, 'error' => $e->getMessage() ) ); … … 229 308 230 309 /** 310 * Get tags attached to a contact. 311 * 312 * This method is used ONLY for first-time sync to discover existing role tags. 313 * After first sync, role tracking uses local user meta to avoid API calls. 314 * 315 * @since 1.0.0 316 * @param string $contact_id Contact ID. 317 * @return array<string> Array of tag names, or empty array on failure. 318 */ 319 private function get_contact_tags( string $contact_id ): array { 320 if ( empty( $contact_id ) ) { 321 return array(); 322 } 323 324 try { 325 $request = new ListTagsAttachedToContactRequest( array( 'contactId' => $contact_id ) ); 326 $tag_list = $this->api->api_call( 'contacts', 'listAttachedTags', $request ); 327 328 if ( ! $tag_list ) { 329 return array(); 330 } 331 332 // Extract tag names from TagList - try multiple access patterns. 333 $tag_names = array(); 334 $tags = array(); 335 336 // Pattern 1: getTags() method 337 if ( method_exists( $tag_list, 'getTags' ) ) { 338 $tags = $tag_list->getTags(); 339 } 340 // Pattern 2: getData() method (paginated response) 341 elseif ( method_exists( $tag_list, 'getData' ) ) { 342 $tags = $tag_list->getData(); 343 } 344 // Pattern 3: tags property 345 elseif ( isset( $tag_list->tags ) ) { 346 $tags = $tag_list->tags; 347 } 348 // Pattern 4: data property (common in Intercom responses) 349 elseif ( isset( $tag_list->data ) ) { 350 $tags = $tag_list->data; 351 } 352 // Pattern 5: Direct array 353 elseif ( is_array( $tag_list ) ) { 354 $tags = $tag_list; 355 } 356 357 foreach ( $tags as $tag ) { 358 if ( method_exists( $tag, 'getName' ) ) { 359 $tag_names[] = $tag->getName(); 360 } elseif ( isset( $tag->name ) ) { 361 $tag_names[] = $tag->name; 362 } elseif ( is_array( $tag ) && isset( $tag['name'] ) ) { 363 $tag_names[] = $tag['name']; 364 } 365 } 366 367 return $tag_names; 368 } catch ( \Exception $e ) { 369 $this->logger->error( 'Failed to get contact tags', array( 370 'contact_id' => $contact_id, 371 'error' => $e->getMessage(), 372 ) ); 373 return array(); 374 } 375 } 376 377 /** 378 * Clear cached tag sync data for a user. 379 * 380 * Forces next sync to fetch existing tags from Intercom. 381 * Useful for fixing sync issues or migrating users to new caching system. 382 * 383 * @since 1.0.0 384 * @param int $user_id WordPress user ID. 385 * @return bool True on success, false on failure. 386 */ 387 public function clear_tag_sync_cache( int $user_id ): bool { 388 $prefix = 'rpplstp_iws'; 389 $result = delete_user_meta( $user_id, $prefix . '_synced_roles' ); 390 391 if ( $result ) { 392 $this->logger->info( 'Tag sync cache cleared for user', array( 'user_id' => $user_id ) ); 393 } 394 395 return $result; 396 } 397 398 /** 399 * Clear tag sync cache for all users. 400 * 401 * Use this to force all users to re-sync with Intercom on next sync. 402 * Useful when migrating to new tag sync system. 403 * 404 * @since 1.0.0 405 * @return int Number of users cleared. 406 */ 407 public function clear_all_tag_sync_caches(): int { 408 global $wpdb; 409 $prefix = 'rpplstp_iws'; 410 411 $result = $wpdb->delete( 412 $wpdb->usermeta, 413 array( 'meta_key' => $prefix . '_synced_roles' ), 414 array( '%s' ) 415 ); 416 417 $count = $result ? (int) $result : 0; 418 $this->logger->info( 'Tag sync cache cleared for all users', array( 'count' => $count ) ); 419 420 return $count; 421 } 422 423 /** 231 424 * Sync tags for a contact based on user data. 425 * 426 * This method ensures that role tags are properly synchronized by: 427 * - Tracking last synced roles in WordPress user meta 428 * - Only making API calls for actual changes (minimizes API usage) 429 * - Removing old role tags that are no longer valid 430 * - Adding new role tags for current roles 232 431 * 233 432 * @since 1.0.0 … … 259 458 260 459 // Sync role tags if enabled. 261 if ( in_array( 'role', $tags_to_sync, true ) && ! empty( $user->roles ) ) { 262 foreach ( $user->roles as $role ) { 263 // Use the role key (slug) with wp_role prefix. 264 $tag_name = 'wp_role_' . $role; 460 if ( in_array( 'role', $tags_to_sync, true ) ) { 461 // Get current WordPress roles. 462 $current_roles = ! empty( $user->roles ) ? $user->roles : array(); 463 sort( $current_roles ); // Sort for consistent comparison. 464 465 // Get last synced roles from user meta. 466 $last_synced_roles = get_user_meta( $user_id, $prefix . '_synced_roles', true ); 467 $is_first_sync = empty( $last_synced_roles ); 468 469 if ( ! is_array( $last_synced_roles ) ) { 470 $last_synced_roles = array(); 471 } 472 473 // On first sync, fetch existing role tags from Intercom to properly identify stale tags. 474 // This is a one-time cost per user to ensure accurate tag cleanup. 475 if ( $is_first_sync ) { 476 $this->logger->info( 'First-time tag sync, fetching existing tags from Intercom', array( 477 'user_id' => $user_id, 478 'contact_id' => $contact_id, 479 ) ); 480 481 $existing_tags = $this->get_contact_tags( $contact_id ); 482 483 // Extract existing role tags (tags starting with 'wp_role_'). 484 foreach ( $existing_tags as $tag ) { 485 if ( strpos( $tag, 'wp_role_' ) === 0 ) { 486 $role = str_replace( 'wp_role_', '', $tag ); 487 $last_synced_roles[] = $role; 488 } 489 } 490 491 $this->logger->debug( 'Discovered existing role tags', array( 492 'user_id' => $user_id, 493 'existing_roles' => $last_synced_roles, 494 ) ); 495 } 496 497 sort( $last_synced_roles ); // Sort for consistent comparison. 498 499 // Check if roles have changed. 500 if ( $current_roles === $last_synced_roles && ! $is_first_sync ) { 501 // No changes, skip API calls (but not on first sync). 502 $this->logger->debug( 'Role tags unchanged, skipping sync', array( 503 'user_id' => $user_id, 504 'roles' => $current_roles, 505 ) ); 506 return true; 507 } 508 509 // Determine which roles were added and removed. 510 $roles_to_remove = array_diff( $last_synced_roles, $current_roles ); 511 $roles_to_add = array_diff( $current_roles, $last_synced_roles ); 512 513 // Remove old role tags. 514 foreach ( $roles_to_remove as $old_role ) { 515 $tag_name = 'wp_role_' . $old_role; 516 $this->logger->debug( 'Removing old role tag', array( 517 'contact_id' => $contact_id, 518 'user_id' => $user_id, 519 'role' => $old_role, 520 'tag' => $tag_name, 521 ) ); 522 if ( ! $this->untag_contact( $contact_id, $tag_name ) ) { 523 $success = false; 524 $this->logger->warning( 'Failed to remove old role tag', array( 525 'contact_id' => $contact_id, 526 'tag' => $tag_name, 527 ) ); 528 } 529 } 530 531 // Add new role tags. 532 foreach ( $roles_to_add as $new_role ) { 533 $tag_name = 'wp_role_' . $new_role; 534 $this->logger->debug( 'Adding new role tag', array( 535 'contact_id' => $contact_id, 536 'user_id' => $user_id, 537 'role' => $new_role, 538 'tag' => $tag_name, 539 ) ); 265 540 if ( ! $this->tag_contact( $contact_id, $tag_name ) ) { 266 541 $success = false; 542 $this->logger->warning( 'Failed to add new role tag', array( 543 'contact_id' => $contact_id, 544 'tag' => $tag_name, 545 ) ); 546 } 547 } 548 549 // Update the stored synced roles if successful. 550 if ( $success ) { 551 update_user_meta( $user_id, $prefix . '_synced_roles', $current_roles ); 552 553 // Log summary of changes. 554 if ( ! empty( $roles_to_remove ) || ! empty( $roles_to_add ) ) { 555 $this->logger->info( 'Role tags synchronized', array( 556 'contact_id' => $contact_id, 557 'user_id' => $user_id, 558 'removed_roles' => array_values( $roles_to_remove ), 559 'added_roles' => array_values( $roles_to_add ), 560 ) ); 267 561 } 268 562 } -
sync-engine-for-intercom/trunk/includes/core/class-action-scheduler-handler.php
r3420647 r3450989 101 101 } 102 102 103 // Register action hooks for background processing. 104 add_action( self::PREFIX . '_sync_user', array( $this, 'process_user_sync' ), 10, 1 ); 105 add_action( self::PREFIX . '_sync_event', array( $this, 'process_event_sync' ), 10, 2 ); 106 107 // Mark hooks as initialized. 108 self::$hooks_initialized = true; 103 // Register action hooks for background processing. 104 add_action( self::PREFIX . '_sync_user', array( $this, 'process_user_sync' ), 10, 1 ); 105 add_action( self::PREFIX . '_sync_event', array( $this, 'process_event_sync' ), 10, 2 ); 106 add_action( self::PREFIX . '_sync_tags', array( $this, 'process_tag_sync' ), 10, 2 ); 107 108 // Mark hooks as initialized. 109 self::$hooks_initialized = true; 109 110 } 110 111 … … 143 144 } 144 145 145 $timestamp = time() + $delay; 146 $action_id = as_schedule_single_action( 147 $timestamp, 146 // Dispatch the action using the appropriate method. 147 $action_id = $this->dispatch_action( 148 148 self::PREFIX . '_sync_user', 149 149 array( $user_id ), 150 self::ACTION_GROUP150 $delay 151 151 ); 152 152 … … 156 156 'action_id' => $action_id, 157 157 'delay' => $delay, 158 'type' => $delay > 0 ? 'scheduled' : 'async', 158 159 ) ); 159 160 } else { … … 189 190 } 190 191 191 $timestamp = time() + $delay; 192 $action_id = as_schedule_single_action( 193 $timestamp, 192 // Dispatch the action using the appropriate method. 193 $action_id = $this->dispatch_action( 194 194 self::PREFIX . '_sync_event', 195 195 array( $event_name, $event_data ), 196 self::ACTION_GROUP196 $delay 197 197 ); 198 198 … … 202 202 'action_id' => $action_id, 203 203 'delay' => $delay, 204 'type' => $delay > 0 ? 'scheduled' : 'async', 204 205 ) ); 205 206 } else { … … 207 208 } 208 209 209 return $action_id; 210 } 211 212 213 /** 214 * Process user sync action (called by Action Scheduler). 210 return $action_id; 211 } 212 213 /** 214 * Schedule tag sync action. 215 * 216 * @since 1.0.0 217 * @param string $contact_id Intercom contact ID. 218 * @param int $user_id WordPress user ID. 219 * @param int $delay Optional delay in seconds before processing (default: 0). 220 * @return int|false Action ID on success, false on failure. 221 */ 222 public function schedule_tag_sync( string $contact_id, int $user_id, int $delay = 0 ): int|false { 223 if ( ! $this->is_available() ) { 224 $this->logger->warning( 'Action Scheduler not available, skipping tag sync', array( 225 'contact_id' => $contact_id, 226 'user_id' => $user_id, 227 ) ); 228 return false; 229 } 230 231 // Validate contact_id (critical dependency). 232 if ( empty( $contact_id ) ) { 233 $this->logger->error( 'Cannot schedule tag sync: contact_id is empty', array( 'user_id' => $user_id ) ); 234 return false; 235 } 236 237 // Check if tag sync is enabled. 238 $enable_tags_sync = get_option( self::PREFIX . '_enable_tags_sync', '0' ); 239 if ( '1' !== $enable_tags_sync ) { 240 $this->logger->debug( 'Tag sync disabled, skipping scheduling', array( 'user_id' => $user_id ) ); 241 return false; 242 } 243 244 // Check rate limit and add delay if needed. 245 $rate_limit_delay = $this->rate_limiter->get_delay(); 246 if ( $rate_limit_delay > 0 ) { 247 $delay = max( $delay, $rate_limit_delay ); 248 $this->logger->debug( 'Rate limit delay added to tag sync', array( 249 'contact_id' => $contact_id, 250 'user_id' => $user_id, 251 'delay' => $delay, 252 ) ); 253 } 254 255 // Dispatch the action using the appropriate method. 256 $action_id = $this->dispatch_action( 257 self::PREFIX . '_sync_tags', 258 array( $contact_id, $user_id ), 259 $delay 260 ); 261 262 if ( $action_id ) { 263 $this->logger->info( 'Tag sync scheduled', array( 264 'contact_id' => $contact_id, 265 'user_id' => $user_id, 266 'action_id' => $action_id, 267 'delay' => $delay, 268 'type' => $delay > 0 ? 'scheduled' : 'async', 269 ) ); 270 } else { 271 $this->logger->error( 'Failed to schedule tag sync', array( 272 'contact_id' => $contact_id, 273 'user_id' => $user_id, 274 ) ); 275 } 276 277 return $action_id; 278 } 279 280 281 /** 282 * Process user sync action (called by Action Scheduler). 215 283 * 216 284 * @since 1.0.0 … … 348 416 ) ); 349 417 350 // Fire action for error handling. 351 do_action( self::PREFIX . '_event_sync_failed', $event_name, $event_data, $e ); 418 // Fire action for error handling. 419 do_action( self::PREFIX . '_event_sync_failed', $event_name, $event_data, $e ); 420 } 421 } 422 423 /** 424 * Process tag sync action (called by Action Scheduler). 425 * 426 * @since 1.0.0 427 * @param string $contact_id Intercom contact ID. 428 * @param int $user_id WordPress user ID. 429 * @return void 430 */ 431 public function process_tag_sync( string $contact_id, int $user_id ): void { 432 $this->logger->info( 'Processing scheduled tag sync', array( 433 'contact_id' => $contact_id, 434 'user_id' => $user_id, 435 ) ); 436 437 // Validate contact_id again (in case it was lost). 438 if ( empty( $contact_id ) ) { 439 $this->logger->error( 'Tag sync skipped: contact_id is empty', array( 'user_id' => $user_id ) ); 440 return; 441 } 442 443 // Check if connection is available. 444 $access_token = RPPLSTP_IWS_Admin_Settings::get_access_token(); 445 if ( empty( $access_token ) ) { 446 $this->logger->warning( 'Scheduled tag sync skipped: No access token', array( 447 'contact_id' => $contact_id, 448 'user_id' => $user_id, 449 ) ); 450 return; 451 } 452 453 // Check if tag sync is still enabled. 454 $enable_tags_sync = get_option( self::PREFIX . '_enable_tags_sync', '0' ); 455 if ( '1' !== $enable_tags_sync ) { 456 $this->logger->debug( 'Tag sync disabled, skipping scheduled action', array( 'user_id' => $user_id ) ); 457 return; 458 } 459 460 // Check rate limit before processing. 461 if ( ! $this->rate_limiter->can_make_request() ) { 462 $delay = $this->rate_limiter->get_delay(); 463 $this->logger->warning( 'Rate limit reached, rescheduling tag sync', array( 464 'contact_id' => $contact_id, 465 'user_id' => $user_id, 466 'delay' => $delay, 467 ) ); 468 // Reschedule with delay. 469 $this->schedule_tag_sync( $contact_id, $user_id, $delay ); 470 return; 471 } 472 473 // Perform the actual tag sync. 474 $tags = new RPPLSTP_IWS_Intercom_Tags(); 475 $result = $tags->sync_tags_for_contact( $contact_id, $user_id ); 476 477 // Record the API request. 478 // Note: sync_tags_for_contact() uses local tracking (user meta) to minimize API calls. 479 // First sync: 1 call to list existing tags + calls for role changes 480 // Subsequent syncs: Only makes API calls for actual role changes 481 // - Remove old role tags (1 call per removed role) 482 // - Add new role tags (1 call per added role) 483 // If roles haven't changed, no API calls are made. 484 // We record one request here as a simplification for rate limiting. 485 if ( $result ) { 486 $this->rate_limiter->record_request(); 487 } 488 489 if ( $result ) { 490 $this->logger->info( 'Scheduled tag sync completed successfully', array( 491 'contact_id' => $contact_id, 492 'user_id' => $user_id, 493 ) ); 494 495 // Fire action for other plugins to hook into. 496 do_action( self::PREFIX . '_tags_synced', $contact_id, $user_id ); 497 } else { 498 $this->logger->error( 'Scheduled tag sync failed', array( 499 'contact_id' => $contact_id, 500 'user_id' => $user_id, 501 ) ); 502 503 // Fire action for error handling. 504 do_action( self::PREFIX . '_tag_sync_failed', $contact_id, $user_id ); 505 } 506 } 507 508 /** 509 * Dispatch an action using either async or scheduled method based on delay. 510 * 511 * @since 1.0.0 512 * @param string $hook The hook name to trigger. 513 * @param array $args Arguments to pass to the hook. 514 * @param int $delay Delay in seconds (0 for immediate). 515 * @return int|false Action ID on success, false on failure. 516 */ 517 private function dispatch_action( string $hook, array $args, int $delay ): int|false { 518 // Use async action for immediate execution, scheduled action for delayed execution. 519 if ( $delay > 0 ) { 520 // Schedule for future execution with delay. 521 $timestamp = time() + $delay; 522 return as_schedule_single_action( 523 $timestamp, 524 $hook, 525 $args, 526 self::ACTION_GROUP 527 ); 528 } else { 529 // Enqueue for immediate async execution. 530 return as_enqueue_async_action( 531 $hook, 532 $args, 533 self::ACTION_GROUP 534 ); 352 535 } 353 536 } … … 416 599 $this->ensure_action_scheduler_loaded(); 417 600 418 // Check if the function exists(Action Scheduler is loaded).419 // This is the primary check - if the function exists, Action Scheduler is loaded.420 if ( ! function_exists( 'as_schedule_single_action' ) ) {601 // Check if the functions exist (Action Scheduler is loaded). 602 // This is the primary check - if the functions exist, Action Scheduler is loaded. 603 if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_enqueue_async_action' ) ) { 421 604 return false; 422 605 } -
sync-engine-for-intercom/trunk/includes/sync/class-event-sync.php
r3420647 r3450989 601 601 $this->ensure_user_synced( $event_data ); 602 602 603 // Try to get Intercom contact ID if we have user_id or email. 604 // Only attempt lookup if we have at least one identifier. 605 if ( ! empty( $event_data['user_id'] ) || ! empty( $event_data['email'] ) ) { 606 // Prefer email lookup as it's more reliable. 607 if ( ! empty( $event_data['email'] ) ) { 608 $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts(); 609 $intercom_id = $intercom_contacts->find_by_email( $event_data['email'] ); 610 if ( $intercom_id ) { 611 $event_data['intercom_id'] = $intercom_id; 612 } 613 } 614 } 603 // Try to get Intercom contact ID using cached lookup to minimize API calls. 604 // Only attempt lookup if we have user_id (needed for cache). 605 if ( ! empty( $event_data['user_id'] ) ) { 606 $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts(); 607 $intercom_id = $intercom_contacts->get_cached_contact_id( 608 $event_data['user_id'], 609 null, // external_id not needed here 610 $event_data['email'] ?? null 611 ); 612 if ( $intercom_id ) { 613 $event_data['intercom_id'] = $intercom_id; 614 } 615 } 615 616 616 617 // Schedule event sync in background. … … 695 696 } 696 697 697 // Lazy load user sync instance. 698 if ( null === $this->user_sync ) { 699 $this->user_sync = new RPPLSTP_IWS_User_Sync(); 700 } 701 702 // Check if user exists in Intercom. 703 $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts(); 704 $user_email = $user->user_email; 705 $intercom_id = $intercom_contacts->find_by_email( $user_email ); 706 707 if ( ! $intercom_id ) { 708 // User doesn't exist in Intercom, sync them now. 709 $this->logger->debug( 'User not found in Intercom, syncing user before event', array( 710 'user_id' => $user_id, 711 'email' => $user_email, 712 ) ); 713 $this->user_sync->schedule_sync( $user_id, 0 ); 714 } else { 715 // User exists, but ensure their data is up-to-date by scheduling a sync. 716 // This ensures profile data (name, phone, avatar, etc.) is current. 717 $this->logger->debug( 'User exists in Intercom, ensuring profile is up-to-date before event', array( 718 'user_id' => $user_id, 719 'intercom_id' => $intercom_id, 720 ) ); 721 $this->user_sync->schedule_sync( $user_id, 0 ); 722 // Add intercom_id to event data for better event attribution. 723 $event_data['intercom_id'] = $intercom_id; 724 } 698 // Lazy load user sync instance. 699 if ( null === $this->user_sync ) { 700 $this->user_sync = new RPPLSTP_IWS_User_Sync(); 701 } 702 703 // Check if user exists in Intercom using cached lookup. 704 $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts(); 705 $user_email = $user->user_email; 706 $intercom_id = $intercom_contacts->get_cached_contact_id( 707 $user_id, 708 null, // external_id not needed here 709 $user_email 710 ); 711 712 if ( ! $intercom_id ) { 713 // User doesn't exist in Intercom, sync them now. 714 $this->logger->debug( 'User not found in Intercom, syncing user before event', array( 715 'user_id' => $user_id, 716 'email' => $user_email, 717 ) ); 718 $this->user_sync->schedule_sync( $user_id, 0 ); 719 } else { 720 // User exists, but ensure their data is up-to-date by scheduling a sync. 721 // This ensures profile data (name, phone, avatar, etc.) is current. 722 $this->logger->debug( 'User exists in Intercom, ensuring profile is up-to-date before event', array( 723 'user_id' => $user_id, 724 'intercom_id' => $intercom_id, 725 ) ); 726 $this->user_sync->schedule_sync( $user_id, 0 ); 727 // Add intercom_id to event data for better event attribution. 728 $event_data['intercom_id'] = $intercom_id; 729 } 725 730 } 726 731 } -
sync-engine-for-intercom/trunk/includes/sync/class-user-sync.php
r3420647 r3450989 244 244 } 245 245 246 try { 247 // Try to find existing contact by externalId first (most reliable). 248 // This prevents duplicates even if email changes. 249 $contact_id = null; 250 if ( ! empty( $user_data['externalId'] ) ) { 251 $contact_id = $this->contacts->find_by_external_id( $user_data['externalId'] ); 252 $this->logger->debug( 'Contact lookup by externalId', array( 253 'user_id' => $user_id, 254 'externalId' => $user_data['externalId'], 255 'found' => ! empty( $contact_id ) 246 try { 247 // Use cached contact lookup to minimize API calls. 248 // This checks user meta first, only queries Intercom if not cached. 249 $contact_id = $this->contacts->get_cached_contact_id( 250 $user_id, 251 $user_data['externalId'] ?? null, 252 $user_data['email'] ?? null 253 ); 254 255 if ( $contact_id ) { 256 // Update existing contact. 257 $this->logger->debug( 'Updating existing Intercom contact', array( 'user_id' => $user_id, 'contact_id' => $contact_id ) ); 258 $result = $this->contacts->update( $contact_id, $user_data ); 259 // Ensure contact_id is in result. 260 if ( is_array( $result ) && isset( $result['contact_id'] ) ) { 261 $contact_id = $result['contact_id']; 262 } 263 } else { 264 // Create new contact. 265 $this->logger->debug( 'Creating new Intercom contact', array( 'user_id' => $user_id ) ); 266 $result = $this->contacts->create( $user_data ); 267 // Get contact ID from result and cache it. 268 if ( is_array( $result ) && isset( $result['contact_id'] ) ) { 269 $contact_id = $result['contact_id']; 270 // Cache the newly created contact_id. 271 update_user_meta( $user_id, 'rpplstp_iws_intercom_contact_id', $contact_id ); 272 } 273 } 274 275 // Schedule tag sync asynchronously if enabled. 276 // This ensures user sync completes quickly and tags are processed in background. 277 if ( ! empty( $contact_id ) ) { 278 $tag_action_id = $this->action_scheduler->schedule_tag_sync( $contact_id, $user_id, 0 ); 279 if ( $tag_action_id ) { 280 $this->logger->debug( 'Tag sync scheduled for user', array( 281 'user_id' => $user_id, 282 'contact_id' => $contact_id, 283 'tag_action_id' => $tag_action_id, 256 284 ) ); 257 285 } 258 259 // Fallback to email search if externalId lookup didn't find a contact. 260 // This handles legacy contacts that may not have externalId set yet. 261 if ( ! $contact_id && ! empty( $user_data['email'] ) ) { 262 $contact_id = $this->contacts->find_by_email( $user_data['email'] ); 263 $this->logger->debug( 'Contact lookup by email (fallback)', array( 264 'user_id' => $user_id, 265 'email' => $user_data['email'], 266 'found' => ! empty( $contact_id ) 267 ) ); 268 } 269 270 if ( $contact_id ) { 271 // Update existing contact. 272 $this->logger->debug( 'Updating existing Intercom contact', array( 'user_id' => $user_id, 'contact_id' => $contact_id ) ); 273 $result = $this->contacts->update( $contact_id, $user_data ); 274 // Ensure contact_id is in result. 275 if ( is_array( $result ) && isset( $result['contact_id'] ) ) { 276 $contact_id = $result['contact_id']; 277 } 278 } else { 279 // Create new contact. 280 $this->logger->debug( 'Creating new Intercom contact', array( 'user_id' => $user_id ) ); 281 $result = $this->contacts->create( $user_data ); 282 // Get contact ID from result. 283 if ( is_array( $result ) && isset( $result['contact_id'] ) ) { 284 $contact_id = $result['contact_id']; 285 } 286 } 287 288 // Sync tags if enabled. 289 if ( ! empty( $contact_id ) ) { 290 $tags = new RPPLSTP_IWS_Intercom_Tags(); 291 $tags->sync_tags_for_contact( $contact_id, $user_id ); 292 } 293 294 $this->logger->info( 'User sync completed successfully', array( 'user_id' => $user_id ) ); 295 return $result; 286 } 287 288 $this->logger->info( 'User sync completed successfully', array( 'user_id' => $user_id ) ); 289 return $result; 296 290 297 291 } catch ( IntercomApiException $e ) { -
sync-engine-for-intercom/trunk/sync-engine-for-intercom.php
r3420647 r3450989 4 4 * Plugin URI: https://ripplestep.com/ 5 5 * Description: Sync WordPress and WooCommerce users, events, tags, and customer data with Intercom in real time. 6 * Version: 1.0.36 * Version: 2.0.1 7 7 * Author: RippleStep 8 8 * Author URI: https://ripplestep.com … … 31 31 32 32 // Define plugin constants. 33 define( 'RPPLSTP_IWS_VERSION', ' 1.0.0' );33 define( 'RPPLSTP_IWS_VERSION', '2.0.1' ); 34 34 define( 'RPPLSTP_IWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 35 35 define( 'RPPLSTP_IWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 147 147 */ 148 148 private function init_hooks(): void { 149 add_action( 'before_woocommerce_init', array( $this, 'declare_woocommerce_hpos_compatibility' ) ); 149 150 add_action( 'admin_init', array( $this, 'check_requirements' ) ); 150 151 … … 164 165 // Initialize chat widget. 165 166 add_action( 'wp_footer', array( $this, 'output_chat_widget' ) ); 167 } 168 169 /** 170 * Declare compatibility with WooCommerce HPOS (High-Performance Order Storage). 171 * 172 * Must be called from the before_woocommerce_init hook. Ensures the plugin 173 * is not flagged as incompatible when HPOS (custom order tables) is enabled. 174 * 175 * @since 1.0.0 176 * @return void 177 */ 178 public function declare_woocommerce_hpos_compatibility(): void { 179 if ( ! class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { 180 return; 181 } 182 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); 166 183 } 167 184
Note: See TracChangeset
for help on using the changeset viewer.