Created
September 4, 2025 18:06
-
-
Save goranefbl/48af0dbadab6c18aff36a640bfc4d114 to your computer and use it in GitHub Desktop.
Klaviyo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Klaviyo Integration | |
| */ | |
| class WPGL_Klaviyo | |
| { | |
| private static $instance = null; | |
| const INTEGRATION_KEY = 'klaviyo'; | |
| public static function init() | |
| { | |
| if (!self::is_active()) { | |
| return; | |
| } | |
| if (self::$instance === null) { | |
| self::$instance = new self(); | |
| } | |
| // Register hooks | |
| add_action('wpgens_loyalty_points_updated', [__CLASS__, 'update_klaviyo_loyalty_points'], 10, 5); | |
| } | |
| private static function is_active() | |
| { | |
| $settings = self::get_settings(); | |
| return isset($settings['enabled']) && $settings['enabled'] === '1' && !empty($settings['api_key']); | |
| } | |
| public static function get_settings() | |
| { | |
| $integrations = get_option('wpgens_loyalty_integrations', '{}'); | |
| $settings = json_decode($integrations, true); | |
| $default_settings = [ | |
| 'enabled' => '0', | |
| 'api_key' => '', | |
| ]; | |
| return isset($settings[self::INTEGRATION_KEY]) | |
| ? wp_parse_args($settings[self::INTEGRATION_KEY], $default_settings) | |
| : $default_settings; | |
| } | |
| public static function save_settings($settings) | |
| { | |
| // Sanitize settings | |
| $sanitized_settings = [ | |
| 'enabled' => isset($settings['enabled']) ? sanitize_text_field($settings['enabled']) : '0', | |
| 'api_key' => isset($settings['api_key']) ? sanitize_text_field($settings['api_key']) : '', | |
| ]; | |
| // Validate enabled is only 0 or 1 | |
| $sanitized_settings['enabled'] = in_array($sanitized_settings['enabled'], ['0', '1']) ? $sanitized_settings['enabled'] : '0'; | |
| // Validate API key format (should be non-empty when enabled) | |
| if ($sanitized_settings['enabled'] === '1' && empty($sanitized_settings['api_key'])) { | |
| return false; | |
| } | |
| $integrations = get_option('wpgens_loyalty_integrations', '{}'); | |
| $all_settings = json_decode($integrations, true) ?: []; | |
| $all_settings[self::INTEGRATION_KEY] = wp_parse_args($sanitized_settings, self::get_settings()); | |
| return update_option('wpgens_loyalty_integrations', wp_json_encode($all_settings)); | |
| } | |
| /** | |
| * Map internal activity type/source to user-facing reason title | |
| * | |
| * @param string $type One of WPGL_Points_Activity_Type::* | |
| * @param string $source One of WPGL_Points_Source_Type::* | |
| * @return string | |
| */ | |
| private static function map_reason_title($type, $source) | |
| { | |
| // Normalize inputs for safety | |
| $type = is_string($type) ? strtoupper($type) : ''; | |
| $source = is_string($source) ? strtoupper($source) : ''; | |
| // Expiration has priority regardless of source | |
| if ($type === WPGL_Points_Activity_Type::EXPIRE || $source === WPGL_Points_Source_Type::POINTS_EXPIRED) { | |
| return 'Points expired'; | |
| } | |
| switch ($source) { | |
| case WPGL_Points_Source_Type::PLACE_ORDER: | |
| return 'Order purchase'; | |
| case WPGL_Points_Source_Type::ORDER_DISCOUNT: | |
| return 'Order discount'; | |
| case WPGL_Points_Source_Type::ORDER_CANCELLED: | |
| return 'Order cancelled'; | |
| case WPGL_Points_Source_Type::ORDER_REFUNDED: | |
| return 'Order refunded'; | |
| case WPGL_Points_Source_Type::PRODUCT_REVIEW: | |
| return 'Product review'; | |
| case WPGL_Points_Source_Type::REFER_FRIEND: | |
| return 'Friend referral'; | |
| case WPGL_Points_Source_Type::MANUAL: | |
| return 'Manual adjustment'; | |
| case WPGL_Points_Source_Type::REGISTRATION: | |
| return 'Registration bonus'; | |
| case WPGL_Points_Source_Type::BIRTHDAY: | |
| return 'Birthday bonus'; | |
| case WPGL_Points_Source_Type::IMPORT: | |
| return 'Imported points'; | |
| default: | |
| // Fallback to Title Case of $type | |
| return ucfirst(strtolower($type)); | |
| } | |
| } | |
| /** | |
| * Compute expiry date (ISO8601) if expiration is enabled. | |
| * - For EXPIRE events, returns the awarded_at timestamp. | |
| * - Otherwise, calculates last_activity + expiration_period days. | |
| * | |
| * @param int $user_id | |
| * @param string $type | |
| * @param string $awarded_at_iso | |
| * @return string|null ISO8601 or null if not applicable | |
| */ | |
| private static function compute_expires_at($user_id, $type, $awarded_at_iso) | |
| { | |
| if (!class_exists('WPGL_Points_Expiry') || !WPGL_Points_Expiry::is_expiration_enabled()) { | |
| return null; | |
| } | |
| // If this is an expiration event, use the event time | |
| if ($type === WPGL_Points_Activity_Type::EXPIRE) { | |
| return $awarded_at_iso; | |
| } | |
| $days = (int) WPGL_Points_Expiry::get_expiration_period(); | |
| if ($days <= 0) { | |
| return null; | |
| } | |
| $last_activity = get_user_meta($user_id, WPGL_Points_Expiry::LAST_ACTIVITY_META_KEY, true); | |
| if (empty($last_activity)) { | |
| $base_ts = time(); | |
| } else { | |
| $base_ts = strtotime($last_activity); | |
| if ($base_ts === false) { | |
| $base_ts = time(); | |
| } | |
| } | |
| $expires_ts = $base_ts + ($days * DAY_IN_SECONDS); | |
| return gmdate('c', $expires_ts); | |
| } | |
| public static function update_klaviyo_loyalty_points($user_id, $new_balance, $points_delta, $type, $source) | |
| { | |
| // Skip no-op updates | |
| if ((int)$points_delta === 0) return; | |
| // 1. Get user email and name | |
| $user = get_userdata($user_id); | |
| if (!$user || !$user->user_email) return; | |
| // 2. Get Klaviyo API settings | |
| $settings = self::get_settings(); | |
| $klaviyo_api_key = $settings['api_key']; | |
| $endpoint = 'https://a.klaviyo.com/api/profile-import'; | |
| // 3. Prepare profile data with names and minimal properties | |
| $profile_data = [ | |
| 'email' => $user->user_email, | |
| 'properties' => [ | |
| 'loyalty_points_balance' => (int)$new_balance, | |
| 'loyalty_points_last_updated' => gmdate('c') | |
| ] | |
| ]; | |
| // Add first name if available | |
| if (!empty($user->first_name)) { | |
| $profile_data['first_name'] = $user->first_name; | |
| } | |
| // Add last name if available | |
| if (!empty($user->last_name)) { | |
| $profile_data['last_name'] = $user->last_name; | |
| } | |
| // 4. Create the full API request data | |
| $data = [ | |
| 'data' => [ | |
| 'type' => 'profile', | |
| 'attributes' => $profile_data | |
| ] | |
| ]; | |
| // 5. Configure headers | |
| $args = [ | |
| 'headers' => [ | |
| 'Authorization' => 'Klaviyo-API-Key ' . $klaviyo_api_key, | |
| 'Content-Type' => 'application/vnd.api+json', | |
| 'Accept' => 'application/vnd.api+json', | |
| 'revision' => '2024-05-15' // Latest revision | |
| ], | |
| 'body' => wp_json_encode($data), | |
| 'timeout' => 15 // Prevent long delays | |
| ]; | |
| // 6. Execute API call (profile import) | |
| $response = wp_remote_post($endpoint, $args); | |
| // 7. Error handling (profile) | |
| if (is_wp_error($response)) { | |
| WPGL_Logger::debug('Klaviyo connection error (profile): ' . $response->get_error_message()); | |
| } else { | |
| $status_code = wp_remote_retrieve_response_code($response); | |
| if ($status_code < 200 || $status_code >= 300) { | |
| $body = wp_remote_retrieve_body($response); | |
| WPGL_Logger::debug("Klaviyo API Error (profile) ($status_code)", ['response' => $body]); | |
| } | |
| } | |
| // 8. Emit event: "Points Updated" | |
| $awarded_at_iso = gmdate('c'); | |
| $reason_title = self::map_reason_title($type, $source); | |
| $expires_at_iso = self::compute_expires_at($user_id, $type, $awarded_at_iso); | |
| $unique_id = hash('sha256', implode('|', [ | |
| strtolower($user->user_email), | |
| (string) $type, | |
| (string) $source, | |
| (int) $points_delta, | |
| (int) $new_balance | |
| ])); | |
| $event_payload = [ | |
| 'data' => [ | |
| 'type' => 'event', | |
| 'attributes' => [ | |
| 'time' => $awarded_at_iso, | |
| 'value' => (int) $points_delta, | |
| 'unique_id' => $unique_id, | |
| 'properties' => array_filter([ | |
| 'delta' => (int) $points_delta, | |
| 'balance_after' => (int) $new_balance, | |
| 'reason_title' => $reason_title, | |
| 'source' => $source, | |
| 'awarded_at' => $awarded_at_iso, | |
| 'expires_at' => $expires_at_iso, | |
| 'unique_id' => $unique_id, | |
| ]), | |
| 'metric' => [ | |
| 'data' => [ | |
| 'type' => 'metric', | |
| 'attributes' => [ | |
| 'name' => 'Points Updated', | |
| ], | |
| ], | |
| ], | |
| 'profile' => [ | |
| 'data' => [ | |
| 'type' => 'profile', | |
| 'attributes' => [ | |
| 'email' => $user->user_email, | |
| ], | |
| ], | |
| ], | |
| ], | |
| ], | |
| ]; | |
| $events_endpoint = 'https://a.klaviyo.com/api/events/'; | |
| $event_args = [ | |
| 'headers' => [ | |
| 'Authorization' => 'Klaviyo-API-Key ' . $klaviyo_api_key, | |
| 'Content-Type' => 'application/vnd.api+json', | |
| 'Accept' => 'application/vnd.api+json', | |
| 'revision' => '2024-05-15', | |
| ], | |
| 'body' => wp_json_encode($event_payload), | |
| 'timeout' => 15, | |
| ]; | |
| $event_response = wp_remote_post($events_endpoint, $event_args); | |
| if (is_wp_error($event_response)) { | |
| WPGL_Logger::debug('Klaviyo connection error (event): ' . $event_response->get_error_message()); | |
| } else { | |
| $event_status = wp_remote_retrieve_response_code($event_response); | |
| if ($event_status < 200 || $event_status >= 300) { | |
| $event_body = wp_remote_retrieve_body($event_response); | |
| WPGL_Logger::debug("Klaviyo API Error (event) ($event_status)", ['response' => $event_body]); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment